Skip to content

external-dns

external-dns creates the Route53 records for this cluster's hostnames from the HTTPRoute, TLSRoute and GRPCRoute objects that already declare them. Without it every hostname is a record made by hand in the AWS console, a step that lives nowhere in the repository and leaves a record pointing at nothing when the workload goes.

At a glance

Namespace external-dns
Depends on External Secrets for its Route53 credential, Gateway API for the HTTPRoutes it reads
If it is down New hostnames get no DNS record. Existing records are left alone
Health check kubectl -n external-dns logs deploy/external-dns --tail=50
Files payload/platform/external-dns/

Configuration

In application.yaml's valuesObject:

Setting Why
Sources gateway-httproute and gateway-grpcroute Every HTTP and gRPC hostname here is a route; the address comes from the route's parent Gateway, so nothing is written down twice. The GRPCRoute is Argo CD's, see CLI access
target annotation on apps-gateway The cluster sits behind NAT, so the Gateway's own address is a LAN one nobody outside can reach. external-dns reads the public address off the Gateway, not the routes, so every *.k8s.wlkr.ch record gets it once. infra-gateway has none: those names stay LAN-only on purpose
Source gateway-tlsroute The agents' SSH hostnames are TLSRoutes, one per session. This version reads TLSRoute v1, the version installed, so each session's record appears with its route and no wildcard record exists to maintain; see SSH listener
Domain filter Nothing outside that subtree is touched
TXT registry with an owner id A companion _externaldns.* TXT record stamps every record it creates, and it only modifies or deletes records carrying that stamp. Hand-made records in the same zone are invisible to it
Policy sync Deleting an HTTPRoute removes its record. Safe only because of the registry; without it sync would happily delete your MX records
--aws-zone-match-parent The records live in the wlkr.ch zone, not a zone of their own
A second entry in domainFilters wollbi.ch is a zone of its own in the same account, for the neighbourhood sites. Without it their routes are ignored rather than refused, which reads as external-dns being down
ssh.wlkr.ch in domainFilters Same zone as k8s.wlkr.ch, a different subtree
Credentials as a file (AWS_SHARED_CREDENTIALS_FILE), not environment variables The environment puts a key that can repoint every hostname into kubectl describe pod, crash dumps and every child process. The ExternalSecret templates an INI credentials key, the only key mounted; the original keys stay in the Secret so nothing still reading them breaks

The credential is a separate IAM user and OpenBao path from cert-manager's — see OpenBao for why. Its policy needs route53:ChangeResourceRecordSets on the hosted zone, plus route53:ListHostedZones and route53:ListResourceRecordSets.

Usage

Nothing beyond the HTTPRoute you were already writing — see Gateway API. The credential is stored once, by make bao-secrets or by hand:

bao kv put kv/external-dns/route53 \
  access-key-id="AKIA..." \
  secret-access-key="..."

Health check

kubectl -n external-dns logs deploy/external-dns --tail=50
dig +short argo.infra.k8s.wlkr.ch
kubectl -n external-dns port-forward deploy/external-dns 7979:7979
curl -s localhost:7979/metrics | grep endpoints_total
# external_dns_source_endpoints_total   8   <- hostnames it can see
# external_dns_registry_endpoints_total 0   <- records it owns

All records are already up to date, there are no changes for the matching hosted zones means it found no zone to change: sources fine, provider not, which on Route53 is usually zone matching.

Pitfalls

A record that will not update is one external-dns does not own

Check for the matching _externaldns. TXT record in Route53. To adopt a hand-made record, create that TXT entry or delete the record and let external-dns recreate it.