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.

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:

  1. Source discovery — ExternalDNS reads all Services of type LoadBalancer, all Ingress resources, and optionally DNSEndpoint CRDs to build a desired set of DNS records.
  2. Provider query — it queries the configured DNS provider to fetch the current set of records in the managed zones.
  3. Diff and apply — it computes the difference and creates, updates, or deletes records to converge on the desired state.
  4. 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.

Policy options: ExternalDNS supports three synchronisation policies: 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"
Ingress hostnames: For Ingress resources, ExternalDNS automatically reads the 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.