Kubernetes ExternalDNS: The Essential Tool for Automated Public DNS Management
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
(typeLoadBalancer
)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:
- Watches
Ingress
andService
resources in Kubernetes - Reads domain annotations (e.g.
external-dns.alpha.kubernetes.io/hostname
) - Gets the public IP from the LoadBalancer or Ingress Controller
- Calls the DNS provider API to create/update A records
- Creates TXT records for ownership and conflict prevention
- 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 accessListHostedZones
is needed for zone discovery
π Production Best Practice:
policy=upsert-only
Defaultsync
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
andsource_errors_total
- Whether
last_sync_timestamp_seconds
is consistently updating- Whether
verified_records
is close tosource_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
- GitHub Project: kubernetes-sigs/external-dns
- Official Docs: ExternalDNS Docs
- Helm Chart: ArtifactHub - external-dns
- AWS IAM Permissions: Route 53 API Permissions
- Prometheus Metrics: ExternalDNS FAQ - Metrics
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.
If you found this post useful, feel free to bookmark, share, or follow my blog at astromen.github.io!