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.

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 fromspec.rules[].hostService(type LoadBalancer) — creates DNS records pointing tostatus.loadBalancer.ingressGateway APIHTTPRoutes — extracts hostnames fromspec.hostnames
For each hostname, ExternalDNS creates:
- An
A(orCNAME) record pointing to the load balancer IP or hostname - A
TXTownership 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
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
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.yaml1# 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:
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: 80ExternalDNS 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):
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: 9090The 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
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# Create the Cloudflare API token secret
kubectl create secret generic cloudflare-api-token \
--from-literal=token=YOUR_CF_API_TOKEN \
-n external-dnsThe 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):
# 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 independentlyFor 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:
# 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.


