Skip to content

fix(external-dns): harden single-replica writer against node failure - #2128

Merged
devantler merged 1 commit into
mainfrom
claude/ha-external-dns
Jun 18, 2026
Merged

fix(external-dns): harden single-replica writer against node failure#2128
devantler merged 1 commit into
mainfrom
claude/ha-external-dns

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Daily AI Assistant

Problem

Coroot flags external-dns:Deployment:external-dns (the cluster external-dns, Cloudflare provider) as "single instance - not resilient to node failure". The cluster is not resource-constrained, so the question is the correct HA shape — not whether we can afford replicas.

Leader-election finding for the pinned version (researched, not guessed)

The pinned chart is external-dns 1.21.1, whose appVersion is external-dns v0.21.0 (both are the latest releases as of this PR).

external-dns does NOT support leader election at this version — or at any version. Evidence:

  • The controller's flag registration (pkg/apis/externaldns/types.go) at v0.21.0 and at master contains no leader-election flag. The only concurrency control is --once (run-once mode).
  • The only occurrence of enable-leader-election anywhere in the external-dns repo (default branch) is in docs/proposal/001-leader-election.md — i.e. it is a design proposal, not an implementation.
  • That proposal's status is not-planned.
  • The chart 1.21.1 values.yaml exposes no replicaCount / replicas / leaderElection / lease key (it offers only deploymentStrategy), and no PDB template.

So --enable-leader-election / a single-active-writer multi-replica mode is not available here.

Why a naive 2-replica config is the wrong fix (the hazard)

external-dns uses a TXT-registry ownership model: every managed record is paired with a _externaldns. TXT carrying a single txtOwnerId (${domain}). Two active replicas share that one owner id, so they would both reconcile the same records and issue duplicate Cloudflare API writes — causing rate-limit churn and records fighting each other (flapping). This is exactly why the repo's validate-replica-floor Kyverno policy exempts external-dns by name ("chart has no replicaCount; >1 replica = duplicate Cloudflare writes + throttling"). Without leader election there is no safe way to keep a second replica passive.

Decision: minimise the node-failure blast radius of a deliberately-single writer

Per the honest fix for a single writer that cannot do active/standby HA at this version:

  1. priorityClassName: system-cluster-critical on the Deployment. external-dns programs the cluster's public DNS; its infrastructure peers CoreDNS and the hcloud-csi controller use the same class. On a node loss the scheduler now places the replacement pod ahead of normal workloads, shrinking the window in which live DNS drifts from desired state. (This is the real node-failure-resilience lever for a single pod: fast reschedule.)
  2. Drain-safe PodDisruptionBudget (maxUnavailable: 1, the platform-wide PDBs with minAvailable: 1 over ≤1-replica workloads make nodes undrainable (blocks ksail recycles & rolling reboots) #1880 pattern) added as a separate manifest because the chart ships no PDB template. It lets a node drain evict and reschedule the single pod cleanly instead of stalling on an un-budgeted pod.
  3. Documented the TXT-registry ownership fence that makes a reschedule safe (the rescheduled pod recognises the records it already owns and reconciles idempotently) — and documented the upgrade path: once external-dns ships leader election (--enable-leader-election, currently not-planned), switch to replicas: 2 + leader election (only the leader writes) + a topologySpreadConstraints, and drop the single-replica note. Track the upstream proposal before bumping the replica count.

No duplicate-writes 2-replica config is shipped.

Changes

  • k8s/providers/hetzner/infrastructure/external-dns/helm-release.yaml — add priorityClassName: system-cluster-critical; document the no-leader-election finding, the duplicate-writes hazard, the TXT-ownership safety, and the upgrade path.
  • k8s/providers/hetzner/infrastructure/external-dns/pod-disruption-budget.yaml — new drain-safe PDB (maxUnavailable: 1).
  • k8s/providers/hetzner/infrastructure/external-dns/kustomization.yaml — register the PDB.

Validation

kubectl kustomize k8s/providers/hetzner/infrastructure/external-dns builds cleanly (exit 0); the rendered HelmRelease carries priorityClassName: system-cluster-critical and the PDB renders with the correct app.kubernetes.io/name|instance: external-dns selector.

🤖 Generated with Claude Code

Coroot flags external-dns:Deployment as "single instance - not resilient
to node failure". external-dns v0.21.0 (chart 1.21.1) has NO leader
election — the multi-replica proposal is status `not-planned` and the
`--enable-leader-election` flag does not exist in the binary — so a 2nd
ACTIVE replica is unsafe: both write to Cloudflare under one txtOwnerId,
causing duplicate writes / rate-limit churn / records fighting. The
validate-replica-floor policy exempts external-dns for this reason.

Instead of a hazardous duplicate-writes 2-replica config, minimise the
node-failure blast radius of the deliberately-single writer:
- priorityClassName: system-cluster-critical so the replacement pod
  reschedules ahead of normal workloads on node loss (matches its peers
  CoreDNS and the hcloud-csi controller), shrinking the DNS-drift window.
- drain-safe PodDisruptionBudget (maxUnavailable: 1, the #1880 pattern;
  chart ships no PDB template) so a node drain cycles the single pod
  cleanly instead of stalling.
- document the TXT-registry ownership fence that makes a reschedule safe
  and the upgrade path to true active/standby HA once external-dns ships
  leader election.

Validated with `kubectl kustomize k8s/providers/hetzner/infrastructure/external-dns`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@devantler
devantler marked this pull request as ready for review June 17, 2026 22:30
@devantler
devantler enabled auto-merge June 17, 2026 22:30
@devantler
devantler added this pull request to the merge queue Jun 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 18, 2026
@devantler
devantler merged commit 5e62690 into main Jun 18, 2026
12 of 14 checks passed
@devantler
devantler deleted the claude/ha-external-dns branch June 18, 2026 09:25
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Jun 18, 2026
@botantler-1

botantler-1 Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.64.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@botantler-1 botantler-1 Bot added the released label Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

1 participant