Kubernetes
12 min readMay 5, 2026

ExternalDNS: Automated DNS Management for Kubernetes Services

When you create a Kubernetes Ingress or LoadBalancer Service, you still have to manually create the DNS record pointing to it. ExternalDNS automates this: it watches your Ingress and Service resources and creates, updates, and deletes DNS records in Route53, Cloudflare, Google Cloud DNS, or any supported provider — keeping DNS in sync with your cluster state.

CO
Coding Protocols Team
Platform Engineering
ExternalDNS: Automated DNS Management for Kubernetes Services

DNS records for Kubernetes services are one of the most common sources of operational toil in platform engineering. Every time a new Ingress is created, someone has to create or update the DNS record. Every time a LoadBalancer gets a new IP after a cluster migration, records go stale. ExternalDNS (kubernetes-sigs project) automates this: it continuously watches Kubernetes resources and synchronises DNS records with your DNS provider.

The implementation is straightforward — ExternalDNS reads hostnames from Ingress and Service resources, creates corresponding DNS records in your provider, and uses TXT ownership records to track which records it manages (so it doesn't clobber records you created manually).


How ExternalDNS Works

ExternalDNS watches three main Kubernetes source types:

  • Ingress — extracts hostnames from spec.rules[].host
  • Service (type LoadBalancer) — creates DNS records pointing to status.loadBalancer.ingress
  • Gateway API HTTPRoutes — extracts hostnames from spec.hostnames

For each hostname, ExternalDNS creates:

  1. An A (or CNAME) record pointing to the load balancer IP or hostname
  2. A TXT ownership record (e.g., "heritage=external-dns,external-dns/owner=my-cluster") that identifies ExternalDNS as the creator

The TXT ownership mechanism prevents ExternalDNS from deleting records it didn't create — safe to deploy in zones that also have manually managed records.


Installation with Route53

IAM Policy

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Action": ["route53:ChangeResourceRecordSets"],
7      "Resource": ["arn:aws:route53:::hostedzone/*"]
8    },
9    {
10      "Effect": "Allow",
11      "Action": [
12        "route53:ListHostedZones",
13        "route53:ListResourceRecordSets",
14        "route53:ListTagsForResource"
15      ],
16      "Resource": ["*"]
17    }
18  ]
19}

Helm Install

bash
1helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
2helm repo update
3
4helm install external-dns external-dns/external-dns \
5  --namespace external-dns \
6  --create-namespace \
7  --values external-dns-values.yaml
yaml
1# external-dns-values.yaml
2provider:
3  name: aws
4
5env:
6  - name: AWS_DEFAULT_REGION
7    value: us-east-1
8
9# Use Pod Identity (EKS) — annotate the ServiceAccount
10serviceAccount:
11  annotations:
12    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-dns-role
13  # Or for Pod Identity:
14  # (Create the PodIdentityAssociation separately)
15
16# Sources to watch
17sources:
18  - ingress
19  - service
20  - gateway-httproute   # Gateway API support (requires Gateway API CRDs installed)
21
22# Only manage records for these domains — critical to avoid touching other hosted zones
23domainFilters:
24  - example.com
25  - internal.example.com
26
27# Only manage records in the production hosted zone
28zoneIdFilters:
29  - Z1234567890ABCDEF
30
31# Policy: sync (create/update/delete to match cluster state)
32# or: upsert-only (create/update but never delete)
33policy: sync
34
35# Registry: txt for ownership tracking (recommended)
36registry: txt
37txtOwnerId: my-eks-cluster   # Unique identifier for this cluster — prevents multi-cluster conflicts
38
39# TXT record prefix (optional — helps distinguish ownership records)
40txtPrefix: "_external-dns."
41
42# Log level
43logLevel: warning
44
45# Interval between sync runs
46interval: 1m
47
48# Annotation filter — only manage Ingresses with this annotation
49# annotationFilter: "external-dns.alpha.kubernetes.io/managed=true"

Route53 DNS Record Creation

With ExternalDNS running, any Ingress with a hostname automatically gets a DNS record:

yaml
1# This Ingress will get a Route53 A/CNAME record for api.example.com
2apiVersion: networking.k8s.io/v1
3kind: Ingress
4metadata:
5  name: api
6  namespace: production
7  annotations:
8    # Optional: override the TTL for this specific record
9    external-dns.alpha.kubernetes.io/ttl: "60"
10    # Optional: set a specific hostname (overrides spec.rules[].host)
11    # external-dns.alpha.kubernetes.io/hostname: api.example.com
12spec:
13  ingressClassName: alb
14  rules:
15    - host: api.example.com    # ExternalDNS creates this record automatically
16      http:
17        paths:
18          - path: /
19            pathType: Prefix
20            backend:
21              service:
22                name: api
23                port:
24                  number: 80

ExternalDNS creates:

  • api.example.com → A record pointing to the ALB hostname (as CNAME if the LB has a hostname, or A record if it has an IP)
  • _external-dns.api.example.com → TXT record with ownership information

LoadBalancer Services

For Services with type: LoadBalancer (NLBs, classic ELBs):

yaml
1apiVersion: v1
2kind: Service
3metadata:
4  name: grpc-api
5  namespace: production
6  annotations:
7    external-dns.alpha.kubernetes.io/hostname: grpc.example.com
8    external-dns.alpha.kubernetes.io/ttl: "300"
9spec:
10  type: LoadBalancer
11  selector:
12    app: grpc-api
13  ports:
14    - port: 443
15      targetPort: 9090

The external-dns.alpha.kubernetes.io/hostname annotation explicitly sets the DNS hostname — useful when you want a different DNS name than the Service name.


Cloudflare Provider

yaml
1# external-dns-values.yaml for Cloudflare
2provider:
3  name: cloudflare
4
5env:
6  - name: CF_API_TOKEN
7    valueFrom:
8      secretKeyRef:
9        name: cloudflare-api-token
10        key: token
11
12# Cloudflare-specific options
13extraArgs:
14  - --cloudflare-proxied=false   # Set to true to enable Cloudflare CDN/proxy for the records
bash
# Create the Cloudflare API token secret
kubectl create secret generic cloudflare-api-token \
  --from-literal=token=YOUR_CF_API_TOKEN \
  -n external-dns

The Cloudflare API token needs Zone:Read and DNS:Edit permissions on the zones ExternalDNS manages.


Multi-Cluster DNS

When multiple clusters create DNS records for the same hostname, conflicts arise. ExternalDNS handles this with the txtOwnerId field — each cluster has a unique owner ID, and TXT records identify which cluster owns each DNS record.

For active-active multi-cluster setups where you want round-robin DNS (multiple A records for the same hostname from different clusters):

yaml
# On each cluster, set a unique txtOwnerId but use the same hostname
policy: sync
registry: txt
txtOwnerId: cluster-us-east-1   # Different per cluster
# Both clusters create the same A record — ExternalDNS manages them independently

For active-passive setups, use policy: upsert-only on the passive cluster to prevent it from deleting records created by the active cluster.


Frequently Asked Questions

ExternalDNS deleted a DNS record I created manually — how do I prevent this?

ExternalDNS only manages records it created (those with a matching TXT ownership record). If a record has no TXT ownership record, ExternalDNS ignores it and won't delete it. The policy: upsert-only option is even safer — it creates and updates records but never deletes them (useful during initial adoption where you're migrating from manual DNS management).

How do I check what ExternalDNS will do without it making changes?

Run ExternalDNS in dry-run mode:

bash
# Add --dry-run flag to see what changes ExternalDNS would make
kubectl exec -n external-dns deploy/external-dns -- \
  external-dns --dry-run --log-level=info ...

Or check the logs after a sync cycle — ExternalDNS logs every record it creates, updates, and deletes.

Can ExternalDNS manage private hosted zones?

Yes. For Route53, ExternalDNS can manage both public and private hosted zones. Use zoneTypeFilter: private to restrict to private zones only, or zoneIdFilters to specify exact zone IDs. For internal services that only need private DNS, filter to the private hosted zone and ensure ExternalDNS has IAM access to it.

What if my DNS provider isn't supported?

ExternalDNS supports 40+ providers, but if yours isn't listed, you can use the webhook provider — a sidecar container that implements the ExternalDNS provider webhook API. This allows custom DNS providers without forking ExternalDNS itself.


For the AWS Load Balancer Controller that creates the ALB/NLB that ExternalDNS points DNS to, see AWS Load Balancer Controller for EKS. For cert-manager that automates TLS certificate provisioning alongside ExternalDNS's DNS automation, see cert-manager in Production.

Automating DNS management across a multi-cluster platform? Talk to us at Coding Protocols — we help platform teams eliminate DNS toil with ExternalDNS integrated into GitOps workflows.

Related Topics

ExternalDNS
DNS
Kubernetes
Route53
AWS
Cloudflare
Platform Engineering
Networking

Read Next