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.

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íticaCreaActualizaBorra
upsert-only (por defecto en el chart)SíSíNo
syncSíSíSí
create-onlySíNoNo

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:

curl -s https://api.cloudflare.com/client/v4/user/tokens/verify \
  -H "Authorization: Bearer tu_token_cloudflare"

La respuesta debe incluir "status": "active":

{"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:

kubectl create namespace external-dns

Guarda el token en un Secret dentro de ese namespace:

kubectl create secret generic cloudflare-api-token \
  --namespace external-dns \
  --from-literal=apiToken='tu_token_cloudflare'

Verifica que el Secret existe:

kubectl get secret cloudflare-api-token -n external-dns
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:

helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update

Crea el archivo de valores:

nano external-dns-values.yaml

Pega esta configuración, cambiando el dominio y el identificador del clúster:

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:

helm install external-dns external-dns/external-dns \
  --namespace external-dns \
  --values external-dns-values.yaml

Comprueba que el pod arranca:

kubectl get pods -n external-dns
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:

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:

nano web-demo.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:

kubectl apply -f web-demo.yaml

Espera a que el Service tenga IP externa:

kubectl get service web-demo
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:

kubectl logs -n external-dns deployment/external-dns | grep demo.midominio.com
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:

dig +short demo.midominio.com @1.1.1.1
203.0.113.25

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.

nano web-demo-ingress.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:

kubectl apply -f web-demo-ingress.yaml

Comprueba que el Ingress tiene dirección y que el nombre ya resuelve:

kubectl get ingress web-demo
dig +short app.midominio.com @1.1.1.1
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:

kubectl delete -f web-demo-ingress.yaml

Tras el siguiente ciclo, los logs muestran action=DELETE y el nombre deja de resolver:

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.

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:

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:

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.