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. helm3 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.
NotaExternalDNS solo publica una dirección cuando el recurso ya la tiene. Un Service de tipo
LoadBalanceren estado<pending>o un Ingress sin dirección enstatusno generan ningún registro.
Cómo funciona ExternalDNS
Cada cierto intervalo (1 minuto por defecto), ExternalDNS:
- Lee los recursos de las fuentes configuradas (
service,ingressy otras). - Calcula qué nombres deberían existir y a qué IP o nombre deben apuntar.
- 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 - ReadyZone - 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 variableCF_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 defectoupsert-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
Consejopara que Cloudflare haga de proxy (nube naranja), añade la anotación
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true". En ese casodigdevolverá 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.
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.
Importanteno 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:
kubectl rollout restart deployment/external-dns -n external-dns
No se crea ningún registro. Comprueba en este orden:
- Que el Service tiene
EXTERNAL-IPo el Ingress tieneADDRESS. 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.
