Automate DNS records with external-dns
external-dns keeps your public DNS in step with the cluster: when a Service or Ingress receives a load balancer address, external-dns creates the matching record at your DNS provider, and when the address changes, it updates it. This removes the need to edit DNS manually every time a load balancer IP changes. It is not part of the platform; you install it as a workload.
Install it with your provider#
Add the chart repository, then install external-dns with a credential Secret for the DNS provider that holds your zones. Cloudflare, AWS Route 53, and Hetzner DNS each take a different credential. Grant it the narrowest credential that works: a token scoped to only the zones it manages.
$ 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#
external-dns writes a companion TXT record next to each record it manages, tagged with an owner id you set (--txt-owner-id). This is how it identifies its own records and avoids modifying records that you or another controller created. Set a distinct owner id per cluster.
For an agency running many client domains, add a domain filter (--domain-filter) so an instance manages only the zones it should, and run separate instances or filters per tenant so one client's controller can never rewrite another client's DNS.
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.