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.)
  • helm v3 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.