Skip to main content

Automate DNS records with external-dns

Inspect 1.36

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.

Annotate what to publish#

external-dns reads a hostname annotation from your Services and Ingresses and publishes a record for it:

yaml
		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 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 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#