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. ```console $ 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: ```console $ kubectl create namespace external-dns $ kubectl create secret generic cloudflare-api-token -n external-dns \ --from-literal=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: ```console $ kubectl create namespace external-dns $ kubectl create secret generic route53-credentials -n external-dns \ --from-literal=aws_access_key_id= \ --from-literal=aws_secret_access_key= $ 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: ```console $ kubectl create namespace external-dns $ kubectl create secret generic hetzner-dns-token -n external-dns \ --from-literal=api-key= $ 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: ```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 [Serve IPv6 clients](/docs/hetzner/apalla/network/load-balancing/serve-ipv6) 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](/docs/hetzner/apalla/security/multi-tenant-isolation-for-agencies) 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 - [Set up a Let's Encrypt ClusterIssuer](/docs/hetzner/apalla/network/dns-certs/lets-encrypt-clusterissuer) - [Configure a Hetzner load balancer](/docs/hetzner/apalla/network/load-balancing/configure-a-load-balancer)