English | δΈ­ζ–‡

In Kubernetes production environments, exposing services to the public internet is common. We usually achieve this with Ingress or LoadBalancer type Service. However, every time we launch a new service or change a domain, manually adding an A record in the cloud provider’s DNS console is inefficient and error-prone.

Is there a way to let DNS records be automatically created, updated, and cleaned up along with service deployment? The answer is: ExternalDNS.

πŸ”— GitHub Project: https://github.com/kubernetes-sigs/external-dns
πŸ“š Official Docs: https://external-dns.github.io/

ExternalDNS, maintained by Kubernetes SIGs, is an open-source controller that automatically syncs Services and Ingress resources to external DNS systems (such as AWS Route 53, Cloudflare, Alibaba Cloud, etc.), enabling a fully automated β€œservice online = domain live” workflow.


What is ExternalDNS?

ExternalDNS runs inside a Kubernetes cluster and continuously watches these resources:

  • Ingress (most common)
  • Service (type LoadBalancer)
  • Gateway (Kubernetes Gateway API support)

When these resources change, ExternalDNS extracts domain info and uses the external DNS provider API to create, update, or delete DNS records (A, CNAME, TXT, etc.).

Key Capabilities

  • βœ… Automatic DNS record synchronization
  • βœ… Support for major cloud and third-party DNS providers
  • βœ… Multi-cluster and multi-tenant support
  • βœ… Fine-grained control via annotations
  • βœ… Seamless GitOps integration

How It Works

The workflow of ExternalDNS:

  1. Watches Ingress and Service resources in Kubernetes
  2. Reads domain annotations (e.g. external-dns.alpha.kubernetes.io/hostname)
  3. Gets the public IP from the LoadBalancer or Ingress Controller
  4. Calls the DNS provider API to create/update A records
  5. Creates TXT records for ownership and conflict prevention
  6. Cleans up DNS records when resources are deleted
graph LR
    A[Ingress / Service] --> B(ExternalDNS Controller)
    B --> C[Cloud DNS Provider]
    C --> D[Public DNS Resolution]

Quickstart: AWS Route 53 Example

1. Install ExternalDNS (Helm)

Helm is recommended for version management and easy configuration.

βœ… Pinned version info (latest stable as of September 2025)

helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm install external-dns external-dns/external-dns \
  --version 1.19.0 \                    # Chart version (appVersion v1.19.0)
  --namespace kube-system \
  --set provider.name=aws \
  --set aws.region=us-west-2 \
  --set txtOwnerId=my-cluster \
  --set domainFilters[0]=example.com \
  --set logLevel=info \
  --set policy=upsert-only              # Recommended for production

πŸ“Œ Version Notes:

  • Helm Chart 1.19.0 maps to ExternalDNS appVersion v1.19.0
  • Kubernetes version: v1.22+ recommended
  • See Helm Chart Releases

2. Configure AWS IAM Permissions

ExternalDNS requires Route 53 permissions. Without them, DNS records cannot be created.

Example IAM Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets"
      ],
      "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
    },
    {
      "Effect": "Allow",
      "Action": [
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets"
      ],
      "Resource": "*"
    }
  ]
}

πŸ” Security Tips:

  • Use IAM Roles bound to nodes or IRSA (IAM Roles for Service Accounts)
  • Avoid long-lived AccessKeys
  • ChangeResourceRecordSets is essential for write access
  • ListHostedZones is needed for zone discovery

πŸ”’ Production Best Practice: policy=upsert-only
Default sync behavior deletes records when resources are removed, which may accidentally wipe non-cluster records.
With --set policy=upsert-only, ExternalDNS only creates/updates and never deletes records, reducing risk.
Deletion should be handled by CI/CD cleanup jobs or manually.


3. Example Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    kubernetes.io/ingress.class: "nginx"
    external-dns.alpha.kubernetes.io/hostname: "app.example.com"
spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-service
                port:
                  number: 80

After deployment, ExternalDNS will:

  • Get the LoadBalancer IP of Nginx Ingress Controller
  • Create an A record for app.example.com in Route 53
  • Create a TXT record at the same hostname (app.example.com) containing:
    "heritage=external-dns,external-dns/owner=my-cluster,external-dns/resource=ingress/my-app"

TXT Records Explained

ExternalDNS creates TXT records for each DNS entry:

πŸ” Ownership & Conflict Prevention

When multiple ExternalDNS instances (e.g. multi-cluster or multi-env) manage the same domain, TXT records (with txtOwnerId) prevent conflicts. ExternalDNS checks TXT records before deleting/updating to avoid overwriting others.

πŸ“Œ In multi-cluster setups, always use distinct txtOwnerId, e.g.:

--set txtOwnerId=prod-cluster
--set txtOwnerId=staging-cluster

Supported Resources & Providers

Supported Resources

Resource Description
Ingress Most common, for HTTP/HTTPS
Service (LoadBalancer) Directly exposes IP, suitable for TCP/UDP
Gateway (Gateway API) Next-gen gateway standard

Supported Providers

Provider Supported
AWS Route 53 βœ…
Google Cloud DNS βœ…
Azure DNS βœ…
Cloudflare βœ…
DigitalOcean βœ…
Alibaba Cloud βœ…
CoreDNS (self-hosted) βœ…
PowerDNS βœ…

Advanced Usage & Best Practices

1. Multiple Hostnames

external-dns.alpha.kubernetes.io/hostname: "web.example.com,api.example.com"

2. Custom TTL

external-dns.alpha.kubernetes.io/ttl: "60"

3. Ignore Certain Resources

external-dns.alpha.kubernetes.io/ignore: "true"

4. Manually Set Target

For non-LoadBalancer cases:

external-dns.alpha.kubernetes.io/target: "1.2.3.4"

⚠️ Notes:

  • Technically, A records can point to private IPs (e.g. 10.x.x.x), but public clients cannot reach them β†’ usually ineffective
  • More suitable for CNAME or private DNS setups
  • Known Issue: With policy=sync, target annotation may be ignored in some versions; policy=upsert-only is safer

Where It Fits / Doesn’t Fit

Scenario Works? Notes
βœ… Public cloud (AWS/GCP/Aliyun) βœ”οΈ Best practice
βœ… CI/CD pipelines βœ”οΈ Domain auto-bound per release
βœ… Multi-env (dev/stg/prod) βœ”οΈ Combine with domainFilters
❌ Private-only clusters ⚠️ Needs private DNS (e.g. CoreDNS)
❌ Ops-managed DNS ❌ May conflict with manual workflows

Debugging & Troubleshooting

1. View Logs

kubectl logs -n kube-system deployment/external-dns

Enable debug logs:

--set logLevel=debug

Sample output:

time="2025-04-05T10:00:00Z" level=debug msg="Adding DNS record: app.example.com -> 203.0.113.10"
time="2025-04-05T10:00:01Z" level=info  msg="Desired change: CREATE app.example.com A [Id: /hostedzone/Z12345]"

2. Dry-run Mode

Use --set dryRun=true to preview intended changes without modifying DNS.

3. Monitoring & Observability (Prometheus Metrics)

ExternalDNS exposes Prometheus metrics at the /metrics endpoint (default port 7979, configurable via --metrics-address). These metrics help monitor DNS sync status, error rates, and record counts.

Common metrics documented in the ExternalDNS FAQ:

Metric Description
external_dns_controller_last_sync_timestamp_seconds Unix timestamp of the last successful sync with the DNS provider
external_dns_registry_endpoints_total Number of DNS records in the registry (the desired state managed by ExternalDNS)
external_dns_registry_errors_total Number of errors when interacting with the DNS provider (e.g., API failures)
external_dns_source_endpoints_total Number of DNS records read from Kubernetes resources (Ingress/Service, i.e., desired state)
external_dns_source_errors_total Number of errors when reading from Kubernetes resources (e.g., API access issues)
external_dns_controller_verified_records Number of DNS records successfully verified to exist at the provider (source matches target)
external_dns_registry_a_records Number of A records in the registry
external_dns_source_a_records Number of A records declared in the Kubernetes cluster

πŸ“Š Recommendations:

  • Configure Prometheus to scrape from http://<external-dns-pod>:7979/metrics
  • Build Grafana dashboards to visualize sync status
  • Focus on:
    • Growth in registry_errors_total and source_errors_total
    • Whether last_sync_timestamp_seconds is consistently updating
    • Whether verified_records is close to source_a_records (indicating healthy sync)

Edge Cases & Caveats

1. Wildcards

external-dns.alpha.kubernetes.io/hostname: "*.example.com"

βœ… Supported by most providers
⚠️ Some (e.g. Cloudflare) restrict second-level wildcards (*.staging.example.com)

2. Targets with Private IPs

external-dns.alpha.kubernetes.io/target: "10.0.0.1"

⚠️ While technically possible, public clients cannot resolve private IPs
βœ… Useful only in private DNS/internal resolution (e.g. CoreDNS inside a VPC)


Summary

ExternalDNS is a key tool for automated DNS management in Kubernetes. By using declarative configs, it decouples service exposure from DNS operations, boosting DevOps efficiency.

Core Value

  • πŸš€ Automation: Service online = Domain live
  • πŸ” Safety: TXT records prevent conflicts, upsert-only avoids accidental deletions
  • 🧩 Flexibility: Multi-cloud, multi-env, multi-tenant support
  • πŸ“Š Observability: Built-in Prometheus metrics
  • πŸ› οΈ Integration: Works with Helm, GitOps, CI/CD

πŸ’‘ One-liner takeaway:
ExternalDNS = Declarative DNS + Automated Ops

βœ… Highly recommended for microservices, SaaS, and CI/CD-heavy architectures.


References


This post is based on ExternalDNS v1.19.0, Helm Chart 1.19.0, Kubernetes v1.22+. Configs and behavior may change, please refer to the official docs for updates.