TL;DR

ExternalDNS watches Services (type LoadBalancer) and Ingresses and creates/updates DNS records automatically in your provider (Route53, Cloud DNS, Azure DNS, etc.). Annotate resources with external-dns.alpha.kubernetes.io/hostname to control which hostnames it manages.

How It Works

ExternalDNS reads hostnames from Service annotations, Ingress rules, and Gateway API HTTPRoutes, then creates matching DNS A/CNAME records in your cloud DNS provider — eliminating manual DNS entry management.

Install with Helm (Route53)

Use IRSA (for EKS) or a service account with Route53 permissions. The --domain-filter restricts ExternalDNS to only manage records under your owned zones.

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

helm upgrade --install external-dns external-dns/external-dns \
  --namespace external-dns \
  --create-namespace \
  --set provider=aws \
  --set aws.region=us-east-1 \
  --set domainFilters[0]=example.com \
  --set policy=upsert-only \        # safe: only create/update, never delete
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::123:role/external-dns-role

# Required IAM policy for Route53
# {
#   "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": ["*"]}
#   ]
# }

Annotating Resources

Control ExternalDNS by annotating Services and Ingresses. Explicit hostname annotations take precedence over hostnames inferred from Ingress rules.

yamlannotated-service.yaml
# Service: creates an A record pointing to the LB IP/hostname
apiVersion: v1
kind: Service
metadata:
  name: myapp
  annotations:
    external-dns.alpha.kubernetes.io/hostname: myapp.example.com
    external-dns.alpha.kubernetes.io/ttl: "300"
spec:
  type: LoadBalancer
  selector:
    app: myapp
  ports:
  - port: 443
---
# Ingress: ExternalDNS reads hostnames from spec.rules automatically
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    external-dns.alpha.kubernetes.io/ttl: "120"
spec:
  rules:
  - host: myapp.example.com          # ExternalDNS picks this up automatically
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 80

Troubleshooting

bashtroubleshoot.sh
# Check ExternalDNS logs for sync activity
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns -f

# Check what sources ExternalDNS found
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns | grep "Adding\|Removing\|Updating"

# Verify the DNS record was created
dig myapp.example.com
nslookup myapp.example.com 8.8.8.8

# Check IAM permissions (EKS/IRSA)
kubectl exec -n external-dns deploy/external-dns -- \
  aws sts get-caller-identity

# Dry-run mode: see what records would be changed without applying
# Set --dry-run flag in the Helm values to preview changes

See also