Skip to content

External-DNS

Watches Kubernetes Ingress and Service resources and reconciles their hostnames into Cloudflare DNS records for the tomoda.life zone. Means there is exactly one place to think about DNS — the Ingress spec — and the actual zone file follows.

Installed by k8s/envs/dev/sys/external-dns/application.yaml, configured by k8s/envs/dev/sys/external-dns/values.yaml.

Chart and source

Field Value
Helm chart external-dns
Repository https://kubernetes-sigs.github.io/external-dns/
Version 1.14.3
Destination namespace external-dns (created by Argo CD)
Argo CD Application external-dns

The Application is multi-source: the chart is pulled from the upstream Helm repo, the values.yaml is sourced from this repo via $values/k8s/envs/dev/sys/external-dns/values.yaml.

What it syncs

From values.yaml:

provider: cloudflare
sources:
  - ingress
  - service
domainFilters:
  - tomoda.life
interval: 1m
policy: sync
registry: txt
txtOwnerId: k8s-dev
  • Provider: Cloudflare. External-DNS talks to the Cloudflare API. There is no Cloud DNS / Route 53 integration here — Cloudflare is the only authoritative source for tomoda.life.
  • Sources: Ingress + Service. Every Ingress host and every LoadBalancer Service's annotated hostname becomes a DNS record. In practice this is dominated by Ingresses (since the cluster has exactly one LoadBalancer Service, the Traefik one).
  • Domain filter. Only tomoda.life and its subdomains are touched. Records outside this zone are ignored, even if they appear in cluster manifests.
  • Policy: sync. External-DNS will both create and delete records to match cluster state. If an Ingress is removed, its DNS record is removed. (The alternative upsert-only would leak records.)
  • Registry: txt with txtOwnerId: k8s-dev. Every managed record gets a sibling TXT record marking ownership. External-DNS refuses to delete records it doesn't own — important when multiple things write to the same Cloudflare zone.

Cloudflare credentials

The Cloudflare API token is not in this repo:

extraEnv:
  - name: CF_API_TOKEN
    valueFrom:
      secretKeyRef:
        name: external-dns-cloudflare-secret
        key: api-token

The Secret external-dns-cloudflare-secret in the external-dns namespace is populated out-of-band (Cloudflare API token with Zone:DNS:Edit permissions for tomoda.life). It is not pulled by External Secrets — this is a one-time bootstrap secret. If you rotate the token, update the K8s Secret directly and restart the External-DNS pod.

Verifying

# Watch what External-DNS is doing
kubectl logs -n external-dns deploy/external-dns -f

# Confirm a record exists
dig +short argo-app.tomoda.life

A successful reconciliation log line names the record, the type, and the owner TXT it wrote. If a record is missing, common causes are: hostname not under tomoda.life, Ingress not yet picked up by Traefik (no LoadBalancer status address yet), or the Cloudflare token lacking permissions.

Operational notes

  • One-minute interval. New Ingresses get DNS within ~60 seconds. Faster polls are possible by lowering interval, but Cloudflare will rate-limit aggressive callers.
  • Adding a new public hostname requires nothing beyond declaring it on an Ingress under tomoda.life. No manual DNS work.
  • Moving a hostname out of the cluster means deleting the Ingress (External-DNS removes the record) and then setting whatever new record you want in Cloudflare directly. Avoid leaving Ingresses behind purely as "DNS holders".