ExternalDNS para Descubrimiento de Servicios en Kubernetes
ExternalDNS sincroniza automáticamente los registros DNS con el estado de los Services e Ingresses de Kubernetes, eliminando la necesidad de gestionar manualmente los registros DNS cada vez que se despliega o escala un servicio. Con ExternalDNS, una simple anotación en un Service o Ingress es suficiente para que el registro DNS correspondiente aparezca en Cloudflare, Route53, PowerDNS o cualquier otro proveedor compatible.
Requisitos Previos
- Kubernetes 1.19+
- Acceso de administrador al clúster
- Cuenta en un proveedor DNS compatible (Cloudflare, Route53, PowerDNS, etc.)
helmv3 instalado- Un Ingress Controller instalado (nginx-ingress, Traefik, etc.) si se quiere usar con Ingresses
Instalación de ExternalDNS
# Agregar el repositorio Helm de ExternalDNS
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
# Ver las opciones disponibles
helm show values external-dns/external-dns
# Crear el namespace
kubectl create namespace external-dns
Configuración con Cloudflare
# Crear el Secret con las credenciales de Cloudflare
kubectl create secret generic cloudflare-api-credentials \
--namespace external-dns \
--from-literal=cloudflare_api_token="tu-cloudflare-api-token"
# El token de Cloudflare necesita estos permisos:
# - Zone:Zone:Read
# - Zone:DNS:Edit
# Crear los valores de configuración de Helm
cat > externaldns-cloudflare-values.yaml << 'EOF'
# Proveedor DNS: Cloudflare
provider:
name: cloudflare
# Variables de entorno para las credenciales
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-credentials
key: cloudflare_api_token
# Fuentes de las que ExternalDNS leerá los registros
sources:
- service
- ingress
# Solo gestionar registros en estas zonas DNS (dominios)
domainFilters:
- midominio.com
- midominio.es
# Política de sincronización:
# - sync: crear y eliminar registros automáticamente
# - upsert-only: solo crear, nunca eliminar
# - create-only: solo crear registros nuevos
policy: sync
# Solo procesar recursos con esta anotación (opcional, recomendado en producción)
annotationFilter: "externaldns.io/managed=true"
# Identificador único para este clúster (para gestión de propiedad)
txtOwnerId: "mi-cluster-produccion"
# Intervalo de sincronización
interval: "1m"
# Tipo de registros a crear (A, CNAME, etc.)
registry: txt
# Logs
logLevel: info
logFormat: json
# Recursos y afinidad
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
EOF
helm install external-dns external-dns/external-dns \
--namespace external-dns \
--values externaldns-cloudflare-values.yaml
# Verificar la instalación
kubectl get pods -n external-dns
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns -f
Configuración con Route53 (AWS)
# Opción 1: Usar credenciales de IAM mediante Secret
kubectl create secret generic aws-credentials \
--namespace external-dns \
--from-literal=aws_access_key_id=AKIAIOSFODNN7EXAMPLE \
--from-literal=aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
cat > externaldns-route53-values.yaml << 'EOF'
provider:
name: aws
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws_access_key_id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws_secret_access_key
- name: AWS_REGION
value: "us-east-1"
sources:
- service
- ingress
domainFilters:
- midominio.com
# Filtrar por ZoneID específico de Route53 (más preciso que domainFilters)
zoneIdFilters:
- Z1234567890ABCD
policy: sync
txtOwnerId: "eks-cluster-prod"
interval: "1m"
EOF
helm install external-dns external-dns/external-dns \
--namespace external-dns \
--values externaldns-route53-values.yaml
# Opción 2: Usar IAM Role para Service Account (IRSA) - recomendado en EKS
# La política IAM necesaria para Route53
cat > externaldns-iam-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource"
],
"Resource": [
"*"
]
}
]
}
EOF
aws iam create-policy \
--policy-name ExternalDNSPolicy \
--policy-document file://externaldns-iam-policy.json
Configuración con PowerDNS
# Configurar ExternalDNS con PowerDNS (para entornos on-premises)
cat > externaldns-powerdns-values.yaml << 'EOF'
provider:
name: pdns
env:
- name: PDNS_API_KEY
valueFrom:
secretKeyRef:
name: powerdns-credentials
key: api_key
- name: PDNS_SERVER_URL
value: "http://powerdns.midominio.local:8081"
extraArgs:
- --pdns-server=http://powerdns.midominio.local:8081
- --pdns-api-key=$(PDNS_API_KEY)
sources:
- service
- ingress
domainFilters:
- midominio.local
- midominio.com
policy: sync
txtOwnerId: "k8s-produccion"
EOF
kubectl create secret generic powerdns-credentials \
--namespace external-dns \
--from-literal=api_key="tu-api-key-powerdns"
helm install external-dns external-dns/external-dns \
--namespace external-dns \
--values externaldns-powerdns-values.yaml
Anotaciones y Filtrado
# Configurar un Service para que ExternalDNS cree su registro DNS
cat > service-con-dns.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: mi-aplicacion
namespace: produccion
annotations:
# Indica a ExternalDNS que gestione este registro
externaldns.io/managed: "true"
# Nombre DNS personalizado (en lugar del nombre del Service)
external-dns.alpha.kubernetes.io/hostname: "app.midominio.com"
# TTL personalizado para el registro
external-dns.alpha.kubernetes.io/ttl: "120"
# Especificar el tipo de acceso (opcional)
external-dns.alpha.kubernetes.io/access: "public"
spec:
selector:
app: mi-aplicacion
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
EOF
kubectl apply -f service-con-dns.yaml
# Configurar un Ingress para DNS automático
cat > ingress-con-dns.yaml << 'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mi-app-ingress
namespace: produccion
annotations:
externaldns.io/managed: "true"
# ExternalDNS crea automáticamente el registro para los hosts del Ingress
external-dns.alpha.kubernetes.io/ttl: "300"
# Crear también alias CNAME (para balanceadores de carga)
external-dns.alpha.kubernetes.io/alias: "true"
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- app.midominio.com
- api.midominio.com
secretName: mi-app-tls
rules:
- host: app.midominio.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- host: api.midominio.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-backend
port:
number: 8080
EOF
kubectl apply -f ingress-con-dns.yaml
# Verificar que ExternalDNS ha creado los registros
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns | grep "Creating record"
Gestión de la Propiedad de Registros DNS
ExternalDNS usa registros TXT para rastrear qué registros pertenecen a cada instancia.
# Verificar los registros TXT de propiedad creados en Cloudflare/Route53
# Aparecerán como: externaldns-<tipo>/<nombre> con valor "heritage=external-dns,external-dns/owner=<txtOwnerId>"
# Si hay conflicto entre dos instancias de ExternalDNS:
# Cada instancia debe tener un txtOwnerId único
# Los registros de otra instancia no se modificarán
# Usar ClusterSecretStore para segregar permisos por namespace
cat > cluster-external-dns-config.yaml << 'EOF'
# Un ExternalDNS por namespace con su propio txtPrefix
# externaldns-produccion (txtPrefix=k8s-prod)
# externaldns-staging (txtPrefix=k8s-staging)
EOF
# Configurar ExternalDNS para usar un prefijo TXT diferente
helm upgrade external-dns-staging external-dns/external-dns \
--namespace external-dns-staging \
--set txtPrefix="k8s-staging-" \
--set txtOwnerId="staging-cluster" \
--set namespaceFilter="staging"
Solución de Problemas
ExternalDNS no crea los registros DNS:
# Ver los logs detallados del pod
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns --tail=100
# Verificar que el Service tiene el tipo correcto (LoadBalancer) y una IP asignada
kubectl get services -n produccion -o wide
# Verificar que las anotaciones son correctas
kubectl get ingress -n produccion -o jsonpath='{.items[*].metadata.annotations}'
# Forzar la sincronización reiniciando el pod
kubectl rollout restart deployment -n external-dns external-dns
Error de permisos en Cloudflare:
# Verificar que el token tiene los permisos necesarios
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer tu-token" | jq .
# Verificar que la zona existe en la cuenta de Cloudflare
curl -X GET "https://api.cloudflare.com/client/v4/zones?name=midominio.com" \
-H "Authorization: Bearer tu-token" | jq .result[].id
Registros duplicados o conflictos:
# Listar todos los registros TXT de propiedad
# En Route53:
aws route53 list-resource-record-sets \
--hosted-zone-id Z1234567890 \
--query "ResourceRecordSets[?Type=='TXT']"
# Eliminar registros huérfanos manualmente desde la consola del proveedor DNS
# y reiniciar ExternalDNS para que recree los registros correctamente
Conclusión
ExternalDNS elimina uno de los puntos de fricción más comunes en la operación de Kubernetes: la gestión manual de registros DNS. Al automatizar la creación y eliminación de registros basándose en el estado del clúster, los equipos de desarrollo pueden desplegar nuevos servicios sin esperar a que el equipo de infraestructura actualice el DNS, acelerando los ciclos de despliegue y reduciendo los errores operacionales.


