Kubernetes External DNS: Automatic DNS Record Management (2026)
ExternalDNS is a Kubernetes controller that synchronises exposed Services and Ingresses with DNS providers automatically. Without it, every time you expose a new service or change an IP address, an engineer must manually update DNS records — a slow, error-prone process. ExternalDNS watches Kubernetes resources and creates, updates, or deletes DNS records in providers like AWS Route 53, Cloudflare, Google Cloud DNS, or Azure DNS, making your cluster truly self-managing from a networking perspective.
Table of Contents
How ExternalDNS Works
ExternalDNS runs as a Deployment in your cluster and polls Kubernetes resources on a configurable interval (default 1 minute). Its reconciliation loop works as follows:
- Source discovery — ExternalDNS reads all Services of type
LoadBalancer, allIngressresources, and optionallyDNSEndpointCRDs to build a desired set of DNS records. - Provider query — it queries the configured DNS provider to fetch the current set of records in the managed zones.
- Diff and apply — it computes the difference and creates, updates, or deletes records to converge on the desired state.
- Ownership via TXT records — to avoid conflicts with manually managed records, ExternalDNS creates companion TXT records that mark which records it owns. It only modifies records it owns.
Supported source types: Service, Ingress, Node, Pod, HTTPRoute (Gateway API), CRD (DNSEndpoint), Connector.
Supported providers include AWS Route 53, Cloudflare, Google Cloud DNS, Azure DNS, DigitalOcean, Linode, Hetzner, PDNS, CoreDNS, and many more.
sync (create, update, and delete records), upsert-only (create and update but never delete), and create-only (only create new records, never modify existing). Use upsert-only initially to avoid accidental deletions while you gain confidence.
Installing ExternalDNS with AWS Route 53
For EKS clusters, the recommended authentication method is IAM Roles for Service Accounts (IRSA), which avoids storing AWS credentials as Kubernetes Secrets.
# Create IAM policy for Route 53 management
cat > external-dns-policy.json <
# external-dns-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.2
args:
- --source=service
- --source=ingress
- --domain-filter=example.com
- --provider=aws
- --aws-zone-type=public
- --registry=txt
- --txt-owner-id=my-cluster
- --policy=upsert-only
- --log-level=info
- --interval=1m
env:
- name: AWS_DEFAULT_REGION
value: us-east-1
kubectl apply -f external-dns-deployment.yaml
# Or via Helm
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm upgrade --install external-dns external-dns/external-dns \
--namespace kube-system \
--set provider=aws \
--set aws.zoneType=public \
--set domainFilters[0]=example.com \
--set txtOwnerId=my-cluster \
--set policy=upsert-only \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::ACCOUNT_ID:role/ExternalDNSRole
Cloudflare Provider Setup
For Cloudflare, ExternalDNS uses an API token. Store it as a Kubernetes Secret and reference it in the Deployment.
# Create the Cloudflare API token secret
kubectl create secret generic cloudflare-api-token \
--namespace=kube-system \
--from-literal=cloudflare_api_token=YOUR_CLOUDFLARE_API_TOKEN
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.2
args:
- --source=service
- --source=ingress
- --domain-filter=example.com
- --provider=cloudflare
- --cloudflare-proxied=true # Enable Cloudflare CDN proxy
- --registry=txt
- --txt-owner-id=my-cluster
- --policy=sync
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: cloudflare_api_token
Setting --cloudflare-proxied=true creates A/AAAA records with Cloudflare's orange-cloud proxy enabled, routing traffic through Cloudflare's CDN and DDoS protection layer. Set to false for DNS-only records that bypass the proxy.
Controlling DNS with Annotations
ExternalDNS reads annotations on Services and Ingresses to determine which DNS names to create and with what settings.
# Service with ExternalDNS annotation
apiVersion: v1
kind: Service
metadata:
name: my-api
annotations:
external-dns.alpha.kubernetes.io/hostname: api.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
type: LoadBalancer
selector:
app: my-api
ports:
- port: 443
targetPort: 8443
# Multiple hostnames on a single Service
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: api.example.com,www.example.com
external-dns.alpha.kubernetes.io/ttl: "60"
# Exclude a Service from ExternalDNS management
metadata:
annotations:
external-dns.alpha.kubernetes.io/exclude: "true"
# Cloudflare-specific: override proxy setting per-resource
metadata:
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec.rules[*].host fields — no annotation needed. It creates A/CNAME records pointing to the Ingress controller's LoadBalancer IP/hostname.
Ingress and LoadBalancer DNS
The most common ExternalDNS use case is automatically creating DNS records for Ingress resources. Combined with cert-manager for TLS, this gives you fully automated HTTPS endpoint management.
# Ingress — ExternalDNS will create A/CNAME for api.example.com and app.example.com
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
tls:
- hosts:
- api.example.com
- app.example.com
secretName: my-app-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
TXT Record Registry and Ownership
ExternalDNS uses TXT records to track ownership of DNS records it manages. This prevents it from accidentally modifying records created by other systems or humans. Each managed A/CNAME record gets a corresponding TXT record like:
# For record: api.example.com → 1.2.3.4
# ExternalDNS also creates:
externaldns-api.example.com TXT "heritage=external-dns,external-dns/owner=my-cluster,external-dns/resource=service/default/my-api"
The --txt-owner-id flag identifies which ExternalDNS instance owns a record — critical when running multiple ExternalDNS instances (e.g., one per cluster) against the same DNS zone.
# Verify TXT ownership records were created
aws route53 list-resource-record-sets \
--hosted-zone-id YOUR_ZONE_ID \
--query "ResourceRecordSets[?Type=='TXT']"
# Check ExternalDNS logs for sync activity
kubectl logs -n kube-system deployment/external-dns --tail=50
Domain Filtering and Policy
In production environments with many DNS zones, filtering is essential to prevent ExternalDNS from touching zones it shouldn't manage.
args:
# Only manage records in these domains
- --domain-filter=prod.example.com
- --domain-filter=api.example.com
# Exclude specific zones by ID (Route 53 hosted zone ID)
- --exclude-domains=internal.example.com
# Only manage records in public zones (for Route 53)
- --aws-zone-type=public
# Annotation filter — only process resources with this annotation
- --annotation-filter=external-dns.alpha.kubernetes.io/managed=true
# Label filter — only process resources with this label
- --label-filter=environment=production
Using --annotation-filter is the safest approach for teams migrating to ExternalDNS — only resources explicitly annotated will have their DNS managed, giving you incremental opt-in control.
Frequently Asked Questions
How long does ExternalDNS take to create a DNS record?
ExternalDNS polls on a configurable interval (default 1 minute). After detecting a new Service or Ingress, it calls the DNS provider API. The record typically appears within 1–2 minutes. DNS propagation to end users depends on the TTL of the record — set a low TTL (60 seconds) during initial setup so changes propagate quickly, then raise it to 300–3600 seconds for stable production records.
Can ExternalDNS manage both public and private DNS zones?
Yes. For Route 53, use --aws-zone-type=private to manage private hosted zones (accessible only within a VPC). You can run two separate ExternalDNS instances — one for public zones and one for private zones — using different service account permissions and annotation filters to control which services go to which zone.
What happens to DNS records when a Service is deleted?
With --policy=sync, ExternalDNS deletes the DNS record when the Service or Ingress is deleted. With --policy=upsert-only, the record is left in place. For production, upsert-only is safer initially because it prevents accidental mass DNS deletions from mis-configured resources.