Automate DNS records with external-dns
external-dns synchronizes DNS records with your Kubernetes cluster. Whenever a Service or Ingress is provisioned with a LoadBalancer address, external-dns creates or updates the corresponding DNS records at your DNS provider eliminating manual record maintenance when IPs change. It is not part of the core platform; you install and manage it as a standard cluster workload.
Read this guide if you are:
- Connecting Kubernetes Services or Ingresses to public DNS providers like Cloudflare, AWS Route 53, or Hetzner DNS (see Install it with your provider).
- Automating DNS record creation and updates from Service annotations and Ingress host rules (see Annotate what to publish).
- Publishing dual-stack
AandAAAArecords for Hetzner Load Balancer IPv4 and IPv6 endpoints (see Publish A and AAAA together). - Isolating DNS management across multiple clusters or tenants using TXT record ownership and domain filters (see Ownership and multi-tenant safety).
Install it with your provider
To install external-dns, add the official Helm chart repository and create a Kubernetes Secret containing your DNS provider credentials. Ensure your provider token or IAM policy is strictly scoped to only the specific DNS zones managed by this cluster.
$ helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
Store a scoped Cloudflare API token, then install pointed at the cloudflare provider:
$ kubectl create namespace external-dns
$ kubectl create secret generic cloudflare-api-token -n external-dns \
--from-literal=token=<CF_API_TOKEN>
$ helm install external-dns external-dns/external-dns \
--namespace external-dns \
--set provider=cloudflare \
--set 'env[0].name=CF_API_TOKEN' \
--set 'env[0].valueFrom.secretKeyRef.name=cloudflare-api-token' \
--set 'env[0].valueFrom.secretKeyRef.key=token'
Create the token with edit rights on only the DNS zones you delegate to it.
Give external-dns an AWS access key, and scope its IAM policy to ChangeResourceRecordSets on the hosted zones it manages:
$ kubectl create namespace external-dns
$ kubectl create secret generic route53-credentials -n external-dns \
--from-literal=aws_access_key_id=<AKIA...> \
--from-literal=aws_secret_access_key=<SECRET>
$ helm install external-dns external-dns/external-dns \
--namespace external-dns \
--set provider=aws \
--set 'env[0].name=AWS_ACCESS_KEY_ID' \
--set 'env[0].valueFrom.secretKeyRef.name=route53-credentials' \
--set 'env[0].valueFrom.secretKeyRef.key=aws_access_key_id' \
--set 'env[1].name=AWS_SECRET_ACCESS_KEY' \
--set 'env[1].valueFrom.secretKeyRef.name=route53-credentials' \
--set 'env[1].valueFrom.secretKeyRef.key=aws_secret_access_key'
Hetzner DNS is not built into external-dns; it runs through the webhook provider. Install external-dns with the Hetzner webhook sidecar and give the sidecar a Hetzner DNS API token:
$ kubectl create namespace external-dns
$ kubectl create secret generic hetzner-dns-token -n external-dns \
--from-literal=api-key=<HETZNER_DNS_TOKEN>
$ helm install external-dns external-dns/external-dns \
--namespace external-dns \
--set provider.name=webhook \
--set provider.webhook.image.repository=ghcr.io/mconfalonieri/external-dns-hetzner-webhook \
--set 'provider.webhook.env[0].name=HETZNER_API_KEY' \
--set 'provider.webhook.env[0].valueFrom.secretKeyRef.name=hetzner-dns-token' \
--set 'provider.webhook.env[0].valueFrom.secretKeyRef.key=api-key'
Create the token in the Hetzner DNS Console, scoped to your DNS zones.
Annotate what to publish
external-dns reads a hostname annotation from your Services and Ingresses and publishes a record for it:
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
For an Ingress, external-dns can also read the host rules directly, so the annotation is optional there.
Publish A and AAAA together
Every Hetzner load balancer receives a public IPv4 and IPv6 address, so publish both an A and an AAAA record to serve IPv6 clients. external-dns creates both when it sees both addresses in status.loadBalancer.ingress. See Serve IPv6 clients for why the IPv6 side needs no change inside the cluster.
Ownership and multi-tenant safety
To track records and prevent accidental overwrites, external-dns creates a companion TXT registry record alongside every DNS entry it provisions. The record includes a configurable owner identifier (--txt-owner-id), ensuring controllers in different clusters or environments only modify their own entries. Always assign a unique owner ID per cluster.
In multi-tenant or agency environments managing multiple customer domains, restrict each external-dns deployment with --domain-filter to permit modifications only on explicit zones. Deploying isolated controller instances per tenant prevents cross-tenant record tampering and ensures clean administrative boundaries.
Run several clusters against one zone
When a production and a staging cluster both publish into example.com zone, each external-dns has to tell its own records apart from its neighbor's. The owner id decides which records a cluster is willing to touch, and the TXT prefix keeps their registry records from colliding on the same name. Owner id alone carries you only as long as no cluster ever contends for a hostname another one already publishes, so set both before the first install. The chart exposes them as values:
| Cluster | txtOwnerId | txtPrefix | domainFilters |
|---|---|---|---|
| Production | prod-eu | prod- | example.com |
| Staging | staging-eu | staging- | staging.example.com |
txtOwnerId: prod-eu
txtPrefix: prod-
domainFilters:
- example.com
policy: sync
Where the topology allows it, give each cluster its own subdomain and filter on that, as the staging row does. The clusters then cannot contend for a record name at all, and the owner id becomes a second line of defense rather than the only one. If the zone also holds records external-dns did not create, set policy: upsert-only so it never deletes anything.
Note
external-dns reconciles on an interval and every provider rate-limits its API. If records lag behind a change, that reflects the interval, not a failure. Do not set the interval so low that you hit the provider's rate limit.
Where to go next
Understand in-cluster DNS (CoreDNS)
How CoreDNS resolves Service names, the ndots cost, and how Syself autoscales it. CoreDNS is platform-managed, so you do not edit or scale it.
Install cert-manager
Install cert-manager as a normal workload so the cluster can request and renew TLS certificates on its own.