General
The Kubernetes ExternalDNS Collision Trap: Why Multi-Cluster Ingress Silently Flips Production DNS Records
calricon / field note
When deploying active-active Kubernetes clusters across AWS regions, platform teams install ExternalDNS to automate Route 53 records. Without unique owner IDs and weighted routing annotations, controllers enter a destructive overwrite war, flipping production DNS every thirty seconds. Discover how elite platform engineers tame multi-cluster DNS.
The Kubernetes ExternalDNS Collision Trap: Why Multi-Cluster Ingress Silently Flips Production DNS Records
In modern cloud-native platform engineering, active-active multi-cluster Kubernetes architectures are the ultimate benchmark of enterprise resilience. To survive regional cloud outages and minimize latency for global users, platform teams deploy twin EKS or GKE clusters across geographically dispersed regions (e.g., us-east-1 and us-west-2) or run parallel Blue/Green clusters during major control-plane version migrations. To automate domain name routing, teams deploy the industry-standard open-source ExternalDNS controller into each cluster, pointing to managed cloud DNS providers like Amazon Route 53, Cloudflare, or Google Cloud DNS. In single-cluster environments, ExternalDNS operates flawlessly. In multi-cluster topologies, however, engineering teams frequently walk into a devastating operational catastrophe: the ExternalDNS Collision Trap. When identical Ingress or Gateway API manifests are deployed across multiple clusters without strict ownership isolation and routing policy annotations, the independent ExternalDNS controllers enter a continuous, destructive overwrite war: flipping public DNS records back and forth between regional load balancers every thirty seconds and dropping half of all global user traffic into connection reset blackouts.
Automating DNS across multiple clusters without unique owner IDs isn't high availability; it's an automated overwrite war. Every thirty seconds, Cluster A overwrites Route 53 to point to its load balancer, and thirty seconds later Cluster B overwrites it back—flapping your public DNS and dropping half your traffic in a continuous loop.
The Mechanics of ExternalDNS and The TXT Registry
To understand why multi-cluster DNS collapses, systems engineers must trace how ExternalDNS claims and verifies ownership over cloud DNS records:
- Declarative Ingress Synchronization: ExternalDNS continuously watches Kubernetes Ingress, Service, and Gateway API resources. When an Ingress manifest declares
host: api.enterprise.com, the controller queries the cloud provider's hosted zone to see if an A or CNAME record forapi.enterprise.comexists. - The Ownership Dilemma: Because multiple automated controllers or human administrators might touch the same DNS zone, ExternalDNS implements a registry mechanism (by default, the TXT registry). Alongside the actual routing record (
api.enterprise.com -> alb-east.amazonaws.com), ExternalDNS creates a companion TXT record:name: api.enterprise.com type: TXT value: "heritage=external-dns,external-dns/owner=my-k8s-cluster,external-dns/resource=ingress/default/api-ingress" - The Synchronization Loop: Every sync period (typically 30 to 60 seconds), ExternalDNS compares the desired state in Kubernetes against the actual records in Route 53. If the target address or metadata does not match, ExternalDNS executes an UPSERT transaction to force Route 53 into compliance.
The Overwrite War: How Symmetrical Manifests Trigger Flapping
The disaster unfolds when an enterprise establishes true infrastructure-as-code symmetry. A platform squad configures GitOps (ArgoCD or Flux) to synchronize the exact same application Helm chart to both Cluster-East (us-east-1) and Cluster-West (us-west-2):
- The Default Helm Configuration: In 90% of enterprise deployments, platform engineers install ExternalDNS using default Helm values. They configure the domain filter (
enterprise.com), but leave the--txt-owner-idparameter empty, set to a generic string (e.g.,--txt-owner-id=k8s), or identical across both clusters. - The Initial Claim: Cluster-East boots up. Its ExternalDNS controller reads the Ingress, sees
api.enterprise.com, and issues a Route 53 API call. It creates an ALIAS record pointingapi.enterprise.comto its local Application Load Balancer (alb-east-1234.us-east-1.elb.amazonaws.com), stamping the TXT record withowner=k8s. - The Counter-Attack: Thirty seconds later, Cluster-West's ExternalDNS controller executes its sync loop. It reads the identical Ingress manifest pointing to its local load balancer (
alb-west-5678.us-west-2.elb.amazonaws.com). It inspects Route 53, sees a TXT record withowner=k8s, and concludes: "I own this record, but the target IP is drifted!"
Cluster-West immediately fires a Route 53 UPSERT, overwriting the A record to point to ALB-West. - The Eternal Flapping Cycle: Thirty seconds later, Cluster-East wakes up, detects that its record was changed, and overwrites Route 53 back to ALB-East.
- The User-Facing Outage: Across the globe, recursive DNS resolvers (Google 8.8.8.8, Cloudflare 1.1.1.1, corporate ISP resolvers) receive conflicting answers with every TTL refresh. Client TCP handshakes and TLS sessions initiated against ALB-East are suddenly routed to ALB-West mid-session, triggering SSL handshake mismatches, HTTP 502 Bad Gateway errors, and dropped mobile connections.
The Deletion Nightmare: When --policy=sync Destroys Everything
The collision escalates from flapping to total service termination when platform teams configure ExternalDNS with the destructive synchronization policy: --policy=sync.
Under --policy=sync, ExternalDNS is authorized to delete DNS records in Route 53 if the corresponding Kubernetes Ingress resource is deleted from the cluster.
If an engineer temporarily disables the Ingress in Cluster-West during maintenance, or if Cluster-West experiences a control plane drain, Cluster-West's ExternalDNS controller sees that the Ingress is missing. Because it shares the generic owner ID, it concludes that api.enterprise.com should no longer exist anywhere in the world. It issues a DELETE request to Route 53, instantly vaporizing the DNS record for Cluster-East as well and taking down global production completely.
Default Helm ExternalDNS vs. Uncoordinated Multi-Cluster vs. Calricon Active-Active Architecture
| Evaluation Dimension | Default Helm Setup | Uncoordinated Multi-Cluster | Calricon Active-Active Architecture |
|---|---|---|---|
| TXT Owner Identification | Generic/Default (--txt-owner-id=default) | Identical strings across clusters | Cryptographically unique, immutable cluster IDs |
| Route 53 Routing Policy | Simple routing (Single record) | Simple routing (Overwrites guaranteed) | Multi-Cluster Latency, Weighted, or Geolocation |
| Synchronization Policy | --policy=sync (Cross-cluster deletes) | --policy=sync (Accidental deletes) | --policy=upsert-only with managed delegation |
| Cross-Cluster Collision Risk | High (Duplicate hostnames collide) | 100% collision rate across active-active | Zero collision (Explicit set-identifier annotations) |
| Disaster Recovery Resilience | Fragile (One delete kills all routing) | Fatal (DNS flips every 30s in failover) | Seamless (Automated Route 53 health failover) |
The Calricon Production Playbook: Bulletproofing Multi-Cluster ExternalDNS
Achieving resilient, conflict-free multi-cluster DNS requires enforcing strict controller identity, utilizing advanced Route 53 routing policies, and establishing declarative ownership boundaries.
Step 1: Enforce Unique, Immutable Cluster Owner IDs
Never deploy ExternalDNS with default or shared owner identifiers. Every single Kubernetes cluster must be assigned a globally unique, immutable --txt-owner-id derived from its cloud account, region, and cluster name:
# ExternalDNS Deployment Arguments for Cluster-East (us-east-1)
args:
- --source=ingress
- --source=service
- --domain-filter=enterprise.com
- --provider=aws
- --registry=txt
- --txt-owner-id=arn:aws:eks:us-east-1:123456789012:cluster/prod-east-01
- --txt-prefix=k8s-txt- # Prevent TXT record collision with naked domain records
- --policy=upsert-only # Prevent accidental cross-cluster record deletionsIn Cluster-West, the owner ID must explicitly reflect its distinct regional identity:
- --txt-owner-id=arn:aws:eks:us-west-2:123456789012:cluster/prod-west-01Step 2: Implement Route 53 Weighted or Latency Routing via Ingress Annotations
To allow multiple clusters to manage the same public hostname (api.enterprise.com) without fighting over a single record, configure Route 53 Multi-Record Routing using ExternalDNS AWS annotations.
In Cluster-East Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
kubernetes.io/ingress.class: "alb"
external-dns.alpha.kubernetes.io/hostname: "api.enterprise.com"
# Route 53 Multi-Cluster Routing Configuration
external-dns.alpha.kubernetes.io/set-identifier: "prod-east-alb"
external-dns.alpha.kubernetes.io/aws-weight: "100"
external-dns.alpha.kubernetes.io/aws-health-check-id: "7a8b9c0d-1234-5678-abcd-ef0123456789"
spec:
rules:
- host: api.enterprise.com
...In Cluster-West Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
kubernetes.io/ingress.class: "alb"
external-dns.alpha.kubernetes.io/hostname: "api.enterprise.com"
external-dns.alpha.kubernetes.io/set-identifier: "prod-west-alb"
external-dns.alpha.kubernetes.io/aws-weight: "100"
external-dns.alpha.kubernetes.io/aws-health-check-id: "1a2b3c4d-5678-9012-efab-cdef12345678"
spec:
rules:
- host: api.enterprise.com
...By declaring explicit set-identifier and aws-weight annotations, Route 53 creates two coexisting, weighted records under api.enterprise.com. ExternalDNS in Cluster-East manages only the record stamped with set-identifier: prod-east-alb, while Cluster-West manages prod-west-alb, achieving seamless active-active load balancing with zero overwrites.
Step 3: Restrict Synchronization to --policy=upsert-only
In multi-cluster environments, ban --policy=sync. Mandate --policy=upsert-only across all ExternalDNS controllers. With upsert-only enabled, ExternalDNS creates and updates records as pods scale, but will never issue DELETE calls to Route 53. If a cluster is drained or decommissioned, its routing records remain intact until an SRE or automated pipeline intentionally adjusts the Route 53 weights, preventing catastrophic automated blackouts.
Step 4: Instrument Prometheus Alerts for Route 53 API Churn
Monitor ExternalDNS metrics to catch DNS flapping wars before TTLs propagate:
- alert: ExternalDnsExcessiveRecordModifications
expr: rate(external_dns_registry_endpoints_total[5m]) > 2
for: 2m
labels:
severity: critical
annotations:
summary: "ExternalDNS is modifying DNS records at an abnormal rate; possible multi-cluster overwrite war detected."
- alert: ExternalDnsReconciliationErrors
expr: rate(external_dns_controller_reconcile_errors_total[2m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "ExternalDNS is encountering reconciliation errors communicating with the cloud DNS API."Automation Without Isolation Is Architectural Arson
Automating DNS synchronization across Kubernetes clusters is essential for operational velocity. But deploying automated controllers with write access to global routing zones without explicit ownership boundaries is the cloud equivalent of giving two automated robots hammers and telling them to redecorate the same wall.
Audit your ExternalDNS deployments today. Assign globally unique --txt-owner-id values to every cluster, adopt Route 53 weighted or latency-based annotations with explicit set-identifiers, downgrade destructive sync policies to upsert-only, and monitor cloud DNS API call volumes. In modern cloud architecture, high availability is not achieved by duplicating systems; it is mastered by ensuring that parallel systems never fight for control of the same truth.