OnCallReady

Lesson 34.11 · Kubernetes: Ingress, Gateway API & Service Mesh · 9 min read

DNS that follows your Services: external-dns

In plain words

Imagine a company phone list that a secretary keeps up to date. Whenever someone changes desks, they do not phone everyone; the secretary sees the move, updates the list, and writes their initials next to every entry they manage, so they never touch numbers that someone else added.

external-dns is that secretary for DNS. It watches sources (Ingresses, Services, Gateway routes) for hostnames and addresses, and writes A or CNAME records to a DNS provider. Next to each record it writes a TXT record that says "owned by this cluster", so it only ever changes records it created. In the lab it talks to the lab DNS server over rfc2136.

Who updates DNS when the IP changes?

A LoadBalancer Service gets an address from the cloud or MetalLB (16.6). An Ingress gets one from its controller. Users type names, so someone has to put shop.lab -> 10.64.0.240 into DNS - and change it when the address changes. external-dns is the controller that does it from what is in the cluster.

What you need to know already: DNS records, zones, TTLs and dig (8.10-8.16), LoadBalancer Services (16.6), the Ingress ADDRESS column (16.19), the cert-manager lesson (HTTP-01 needs DNS).

What it reads, where it writes

sources                       ->   external-dns   ->   provider
Service (type LoadBalancer)        computes the        Route 53, Azure DNS, Cloud DNS,
  + annotation hostname            records it wants    Cloudflare, an RFC 2136 server...
Ingress (rules[].host)             and diffs them
HTTPRoute (hostnames)              with the zone

Since v0.22 (2026) the annotation prefix is external-dns.kubernetes.io/, with no fallback: manifests still using the old external-dns.alpha.kubernetes.io/hostname are silently ignored unless the deployment sets --annotation-prefix back. That one bites every upgrade.

Ownership: the TXT registry

Two clusters, or a human, may manage the same zone. external-dns must never delete what it did not create. With --registry=txt --txt-owner-id=oncall-lab it writes a TXT record next to every record it owns:

$ dig +short TXT a-shop.lab
"heritage=external-dns,external-dns/owner=oncall-lab,external-dns/resource=ingress/shop/shop"

A record that exists without that TXT (made by hand) is left alone - and your Ingress simply never gets its name. The logs say nothing loud about it.

Policies

--policy=upsert-only   create and update, never delete (the lab and most teams start here)
--policy=sync          also delete records whose source is gone (cleaner, riskier)

With sync, deleting an Ingress removes its DNS name at the next interval - including when someone deleted the wrong Ingress.

What it logs

$ kubectl logs -n external-dns deploy/external-dns --tail=4
time="2026-10-07T10:00:36Z" level=info msg="Adding RR: shop.lab 300 A 10.64.0.240"
time="2026-10-07T10:00:36Z" level=info msg="Adding RR: a-shop.lab 300 TXT \"heritage=external-dns,external-dns/owner=oncall-lab,external-dns/resource=ingress/shop/shop\""
time="2026-10-07T10:00:36Z" level=info msg="1 record(s) were successfully updated"
time="2026-10-07T10:01:06Z" level=info msg="All records are already up to date"

It reconciles every --interval (30 s in the lab, 1 m by default), and DNS caches add their TTL on top - "I created the Ingress a minute ago and dig still says NXDOMAIN" is usually just the negative-cache TTL (8.14).

What you can now do:

Why it helps

Without automation, DNS is the step everyone forgets: the new Ingress works, but the name still points to the old load balancer, or nowhere. external-dns removes that ticket and keeps records in step with the cluster.

It is also a source of incidents, and knowing how it decides is what makes them short: the TXT ownership records explain why it refuses to touch an existing record, the policy (upsert-only or sync) explains why a deleted Ingress did or did not remove its record, and its log says exactly what it added or skipped. During a migration to Gateway API, it is also the tool that flips a name from the old edge to the new one.

Commands in this lesson

dig kubectl

FAQ

Why does external-dns not overwrite my existing record?

Because there is no TXT ownership record saying this instance created it. external-dns only changes records it owns, so a record someone created by hand stays untouched. You either delete the manual record or create the matching ownership record, deliberately.

What is the difference between upsert-only and sync?

With upsert-only, external-dns creates and updates records but never deletes them, which is the safe default. With sync it also deletes records it owns when the source object disappears. Sync keeps DNS tidy; upsert-only protects you from a bad change wiping out names.

Which address does it publish for an Ingress?

The address in the Ingress's status, which the ingress controller writes, usually the controller's LoadBalancer IP or hostname. If the status is empty (no controller has picked the Ingress up), there is nothing to publish, which is a common reason for a missing record.

How long until a record appears?

external-dns syncs on an interval (one minute by default) and then the DNS TTL applies to anyone who already cached the old answer. During a cutover, lower the TTL well in advance so clients pick up the change quickly, and watch the external-dns log for the record it added.

Is external-dns part of Kubernetes?

It is a Kubernetes SIGs project, installed separately like any controller. It supports many providers (cloud DNS services, rfc2136 for classic DNS servers, and others), configured with flags for the sources, the provider, the domain filter and the owner id.

In an interview Mid

How does external-dns know which DNS records it is allowed to change?

It uses a TXT registry: next to every record it creates, it writes a TXT record with its owner id, for example heritage=external-dns,external-dns/owner=<id> and the resource it came from. Before changing or deleting a record it checks that ownership record, so manually created names and records owned by another cluster are left alone. Together with the policy, upsert-only to never delete or sync to also clean up, that is what makes it safe to run against a shared zone; its log line, such as Adding RR, shows each change.

Also asked: What sources and providers does external-dns work with? · Why would a new Ingress get no DNS record? · How would you use DNS to cut traffic over from one ingress to another?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.