--- title: "Cómo configurar ExternalDNS en Kubernetes con Cloudflare" description: "Instala ExternalDNS con Helm para que Kubernetes cree y borre registros DNS en Cloudflare a partir de tus Services e Ingresses, con control de propiedad TXT." category: "Ecosistema Kubernetes" language: es source: https://cubepath.com/es/docs/ecosistema-kubernetes/external-dns-descubrimiento-de-servicios-kubernetes updated: 2026-09-25 --- # Cómo configurar ExternalDNS en Kubernetes con Cloudflare ExternalDNS es un controlador de Kubernetes que lee los Services e Ingresses del clúster y mantiene sincronizados los registros DNS en un proveedor externo. En lugar de crear a mano un registro A cada vez que publicas una aplicación, añades una anotación o un host al Ingress y ExternalDNS se encarga del resto. En esta guía instalarás ExternalDNS con Helm usando Cloudflare como proveedor, publicarás un Service y un Ingress de prueba y comprobarás que los registros se crean y se eliminan solos. ## Requisitos previos - Un clúster de Kubernetes 1.27 o superior con acceso de administrador desde `kubectl`, por ejemplo un clúster Kubernetes gestionado de CubePath. - `helm` 3 instalado en tu equipo. - Un dominio cuya zona DNS esté gestionada en Cloudflare. En esta guía se usa `midominio.com`; sustitúyelo por tu dominio. - Una forma de asignar IP externas: un controlador de LoadBalancer (el del proveedor, MetalLB, etc.) y, si vas a usar Ingresses, un Ingress Controller como ingress-nginx o Traefik. > Nota: ExternalDNS solo publica una dirección cuando el recurso ya la tiene. Un Service de tipo `LoadBalancer` en estado `` o un Ingress sin dirección en `status` no generan ningún registro. ## Cómo funciona ExternalDNS Cada cierto intervalo (1 minuto por defecto), ExternalDNS: 1. Lee los recursos de las fuentes configuradas (`service`, `ingress` y otras). 2. Calcula qué nombres deberían existir y a qué IP o nombre deben apuntar. 3. Compara con lo que hay en el proveedor DNS y aplica las diferencias. Para no pisar registros creados a mano o por otro clúster, ExternalDNS acompaña cada registro que gestiona con un registro TXT que contiene su identificador de propietario (`txtOwnerId`). Solo modifica o borra registros cuyo TXT le pertenece. La política de sincronización decide si puede borrar: | Política | Crea | Actualiza | Borra | |----------|------|-----------|-------| | `upsert-only` (por defecto en el chart) | Sí | Sí | No | | `sync` | Sí | Sí | Sí | | `create-only` | Sí | No | No | ## Paso 1: Crear un token de API en Cloudflare ExternalDNS necesita un token con permisos mínimos sobre tu zona. En el panel de Cloudflare ve a **My Profile > API Tokens > Create Token**, elige la plantilla **Edit zone DNS** y configura: - Permisos: `Zone - Zone - Read` y `Zone - DNS - Edit`. - Zone Resources: `Include - Specific zone - midominio.com`. Guarda el token, solo se muestra una vez. Comprueba que es válido antes de continuar, sustituyendo `tu_token_cloudflare`: ```bash curl -s https://api.cloudflare.com/client/v4/user/tokens/verify \ -H "Authorization: Bearer tu_token_cloudflare" ``` La respuesta debe incluir `"status": "active"`: ```output {"result":{"id":"...","status":"active"},"success":true,"errors":[],"messages":[{"code":10000,"message":"This API Token is valid and active","type":null}]} ``` ## Paso 2: Guardar el token en un Secret Crea un namespace dedicado para ExternalDNS: ```bash kubectl create namespace external-dns ``` Guarda el token en un Secret dentro de ese namespace: ```bash kubectl create secret generic cloudflare-api-token \ --namespace external-dns \ --from-literal=apiToken='tu_token_cloudflare' ``` Verifica que el Secret existe: ```bash kubectl get secret cloudflare-api-token -n external-dns ``` ```output NAME TYPE DATA AGE cloudflare-api-token Opaque 1 5s ``` ## Paso 3: Preparar los valores de Helm Añade el repositorio oficial del chart, mantenido por el proyecto kubernetes-sigs: ```bash helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/ helm repo update ``` Crea el archivo de valores: ```bash nano external-dns-values.yaml ``` Pega esta configuración, cambiando el dominio y el identificador del clúster: ```yaml provider: name: cloudflare env: - name: CF_API_TOKEN valueFrom: secretKeyRef: name: cloudflare-api-token key: apiToken sources: - service - ingress domainFilters: - midominio.com policy: sync registry: txt txtOwnerId: cluster-produccion interval: 1m logLevel: info ``` Qué hace cada opción: - `provider.name`: el proveedor DNS. ExternalDNS lee el token de la variable `CF_API_TOKEN`. - `sources`: los tipos de recurso que vigila. - `domainFilters`: limita ExternalDNS a esta zona, aunque el token tuviera acceso a más. - `policy: sync`: permite borrar registros cuando eliminas el Service o el Ingress. Si prefieres que nunca borre nada, deja el valor por defecto `upsert-only`. - `txtOwnerId`: identificador único de este clúster. Si varios clústeres comparten zona, cada uno debe tener un valor distinto. ## Paso 4: Instalar ExternalDNS Instala el chart en el namespace creado: ```bash helm install external-dns external-dns/external-dns \ --namespace external-dns \ --values external-dns-values.yaml ``` Comprueba que el pod arranca: ```bash kubectl get pods -n external-dns ``` ```output NAME READY STATUS RESTARTS AGE external-dns-6b8c9d7f5d-x2kqp 1/1 Running 0 30s ``` Revisa los logs para confirmar que se autentica contra Cloudflare y encuentra la zona: ```bash kubectl logs -n external-dns deployment/external-dns ``` Si no hay recursos que publicar todavía, verás mensajes como `All records are already up to date` y ningún error de autenticación. ## Paso 5: Publicar un Service de tipo LoadBalancer Crea una aplicación de prueba con un Service anotado. La anotación `external-dns.alpha.kubernetes.io/hostname` indica el nombre que debe apuntar a la IP externa del Service: ```bash nano web-demo.yaml ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 1 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:stable ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-demo annotations: external-dns.alpha.kubernetes.io/hostname: demo.midominio.com external-dns.alpha.kubernetes.io/ttl: "120" spec: type: LoadBalancer selector: app: web-demo ports: - port: 80 targetPort: 80 ``` Aplica el manifiesto: ```bash kubectl apply -f web-demo.yaml ``` Espera a que el Service tenga IP externa: ```bash kubectl get service web-demo ``` ```output NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web-demo LoadBalancer 10.43.18.201 203.0.113.25 80:31234/TCP 40s ``` En el siguiente ciclo, ExternalDNS crea el registro. Búscalo en los logs: ```bash kubectl logs -n external-dns deployment/external-dns | grep demo.midominio.com ``` ```output level=info msg="Changing record." action=CREATE record=demo.midominio.com ttl=120 type=A zone=... level=info msg="Changing record." action=CREATE record=... type=TXT zone=... ``` Y comprueba la resolución pública: ```bash dig +short demo.midominio.com @1.1.1.1 ``` ```output 203.0.113.25 ``` > Consejo: para que Cloudflare haga de proxy (nube naranja), añade la anotación `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"`. En ese caso `dig` devolverá IP de Cloudflare y el TTL lo gestiona Cloudflare. ## Paso 6: Publicar los hosts de un Ingress Con la fuente `ingress` activada no hace falta ninguna anotación: ExternalDNS crea un registro por cada `host` de las reglas del Ingress, apuntando a la dirección que el Ingress Controller publica en `status.loadBalancer`. ```bash nano web-demo-ingress.yaml ``` ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-demo spec: ingressClassName: nginx rules: - host: app.midominio.com http: paths: - path: / pathType: Prefix backend: service: name: web-demo port: number: 80 ``` Ajusta `ingressClassName` a la clase de tu controlador (`kubectl get ingressclass`) y aplica: ```bash kubectl apply -f web-demo-ingress.yaml ``` Comprueba que el Ingress tiene dirección y que el nombre ya resuelve: ```bash kubectl get ingress web-demo dig +short app.midominio.com @1.1.1.1 ``` ```output NAME CLASS HOSTS ADDRESS PORTS AGE web-demo nginx app.midominio.com 203.0.113.10 80 1m 203.0.113.10 ``` Si el Ingress Controller publica un nombre en lugar de una IP, ExternalDNS crea un CNAME. Para forzar un destino concreto usa la anotación `external-dns.alpha.kubernetes.io/target`. ## Paso 7: Comprobar el borrado y la propiedad Con `policy: sync`, al eliminar los recursos ExternalDNS borra sus registros. Elimina el Ingress: ```bash kubectl delete -f web-demo-ingress.yaml ``` Tras el siguiente ciclo, los logs muestran `action=DELETE` y el nombre deja de resolver: ```bash dig +short app.midominio.com @1.1.1.1 ``` La salida vacía indica que el registro ya no existe (puede tardar lo que dure el TTL en cachés intermedias). Los registros que ExternalDNS gestiona van acompañados de un TXT cuyo valor incluye `heritage=external-dns,external-dns/owner=cluster-produccion`. Puedes verlos en el panel de Cloudflare, en la sección **DNS** de la zona. Los registros que ya existían y no tienen ese TXT no se tocan, por lo que puedes activar ExternalDNS en una zona con registros creados a mano sin riesgo de perderlos. > Importante: no borres a mano los registros TXT de propiedad. Sin ellos, ExternalDNS deja de considerar suyo el registro y no lo actualizará ni lo eliminará. ## Solución de problemas **El pod está en `CrashLoopBackOff` o los logs muestran errores de autenticación.** Revisa que el Secret tiene la clave `apiToken` y que el token es válido con el comando del paso 1. Tras corregir el Secret, reinicia el despliegue: ```bash kubectl rollout restart deployment/external-dns -n external-dns ``` **No se crea ningún registro.** Comprueba en este orden: - Que el Service tiene `EXTERNAL-IP` o el Ingress tiene `ADDRESS`. Sin dirección no hay registro. - Que el nombre pertenece a un dominio incluido en `domainFilters`. - Que no existe ya un registro con ese nombre creado a mano: ExternalDNS no lo sobrescribe porque no es suyo. Bórralo en Cloudflare y espera al siguiente ciclo. Para ver más detalle, sube temporalmente el nivel de log: ```bash helm upgrade external-dns external-dns/external-dns \ --namespace external-dns \ --values external-dns-values.yaml \ --set logLevel=debug ``` **Dos clústeres se pelean por los mismos nombres.** Cada instancia de ExternalDNS que escribe en la misma zona necesita un `txtOwnerId` distinto. Si además quieres separar los TXT de cada entorno, define `txtPrefix` (por ejemplo `staging-`) en los valores de esa instancia. ## Conclusión Has instalado ExternalDNS con Helm, lo has conectado a Cloudflare con un token de permisos mínimos y has comprobado que los registros de Services e Ingresses se crean y se eliminan de forma automática, protegidos por registros TXT de propiedad. A partir de aquí puedes combinarlo con cert-manager para emitir certificados TLS para los mismos hosts, limitar las fuentes a Ingresses si no expones Services directamente, o desplegar una instancia por entorno con su propio `txtOwnerId`.