A single Hetzner region is one failed power feed away from taking every app offline — and until this summer, the self-hosted answer to "what if the datacenter burns down" was to rent it from the hyperscaler you were trying to leave.
On July 18, 2026 the CNCF moved k8gb — the Kubernetes Global Balancer from Sandbox (March 30, 2021) to Incubating (announced August 5). k8gb does global server load balancing with plain DNS — CoreDNS plus external-dns, no Route 53 health checks, no Cloudflare LB, no traffic proxied through a third party. Two Cluster API clusters in different Hetzner regions watch each other over DNS and fail over one hostname. It is the first cloud-neutral GSLB to clear the incubating bar.
Here is the answer you came for, with the numbers early so you can find your row before you read the why.
The answer in one page: one Gslb CR, one delegation, two tables
The setup: two CAPH-managed clusters — eu-nbg1 (Nuremberg) and eu-fsn1 (Falkenstein) — each running k8gb and exposing CoreDNS on a LoadBalancer IP. One parent DNS zone you already own, cloud.example.com, delegated to k8gb. One hostname, app.cloud.example.com, owned by a single Gslb custom resource.
apiVersion: k8gb.io/v1beta1
kind: Gslb
metadata:
name: app
namespace: prod
spec:
ingress:
ingressClassName: nginx
resourceRef:
apiVersion: networking.k8s.io/v1
kind: Ingress
matchLabels:
app: my-app
strategy:
type: failover
primaryGeoTag: eu-nbg1
dnsTtlSeconds: 30
splitBrainThresholdSeconds: 300That is the whole API. resourceRef selects the Ingress (or Gateway/Service) whose external IP becomes the GLB target. strategy.type: failover means eu-nbg1 answers traffic until its probes fail; then eu-fsn1 takes over. Change it to roundRobin and both answer. Add weight: {eu-nbg1: 80, eu-fsn1: 20} and you get weighted traffic. The controller in each cluster reconciles the same object, writes a DNSEndpoint CR, and external-dns syncs the EdgeDNS zone while the CoreDNS plugin serves the delegated zone.
The delegation chain every query walks:
cloud.example.com (your EdgeDNS — any provider you already use)
└─ NS: glb.cloud.example.com → gslb-ns-eu-nbg1.cloud.example.com (A 65.21.1.10)
→ gslb-ns-eu-fsn1.cloud.example.com (A 65.108.2.20)
└─ CoreDNS on each cluster is authoritative for glb.cloud.example.com
└─ A app.glb.cloud.example.com → 65.21.1.10 (healthy) or 65.108.2.20 (failover)No Route 53 hosted zone holds the load-balancing logic. The logic lives in the two controllers gossiping via DNS TXT heartbeats. EdgeDNS only stores NS glue records — it can be Route 53, Cloudflare, Azure DNS, GCP Cloud DNS, NS1, or RFC2136/BIND/Infoblox. You pick the DNS you already pay for; k8gb does not add a per-query meter of its own.
Two tables tell you what you actually get: how long you stay dark, and what you pay.
How long until failover completes — k8gb DNS TTL vs Route 53 health-check interval
This is the "what if the datacenter burns down" RTO. It is always probe failure plus DNS propagation, never instant. Clients cache DNS for the TTL you set; lowering TTL shortens recovery but raises query volume.
k8gb dnsTtlSeconds | Probe detects failure | DNS TTL must expire on client + resolver | Typical client retry window | Total RTO (p50) | Route 53 equivalent (for comparison) |
|---|---|---|---|---|---|
| 15s | ~5–10s (kube readiness) | 15s | 0–15s | ~20–40s | Route 53: 10s interval × 3 consecutive failures = ~30s + TTL |
| 30s (default) | ~5–10s | 30s | 0–30s | ~35–70s | Route 53: 30s interval × 3 = ~90s + TTL |
| 60s | ~5–10s | 60s | 0–60s | ~65–130s | Route 53: 10s fast check still ~30s, but many teams use 30s to save $0.50/check/mo |
k8gb matches Route 53's failure detection at the aggressive 10-second check interval and beats the common 30-second interval most teams actually run to avoid health-check cost. The floor in both systems is DNS: a client that cached app.cloud.example.com 29 seconds before you failed over will not ask again until its cache expires. That is why k8gb defaults to 30 seconds — half the survey-reported pain threshold where users notice — and why teams that need sub-30-second RTO push to 15 or 5 seconds knowing query counts will double or 6x. A splitBrainThresholdSeconds: 300 guard prevents flapping when clusters lose sight of each other but are both healthy.
What you pay per month — at three query volumes
Assumes one hostname, one health check, one hosted zone. k8gb reuses the DNS provider and bandwidth you already have — no per-query metering of its own.
Traffic to app.cloud.example.com | AWS Route 53 (1 zone + 1 health check) | Cloudflare Load Balancer | k8gb on your own DNS |
|---|---|---|---|
| 1M DNS queries / mo | $0.50 (zone) + $0.50 (check) + $0.40 (queries) = $1.40 | $5 (Pro plan LB) on top of DNS | $0 extra — uses existing CoreDNS + EdgeDNS you already run |
| 10M queries / mo | $0.50 + $0.50 + $4.00 = $5.00 | $5–$10 (LB + overages past included) | $0 extra |
| 100M queries / mo | $0.50 + $0.50 + $40.00 = $41.00 | $20–$200 (Advanced tier scales by rules + traffic) | $0 extra |
| Lock-in | Hosted zone lives in your AWS account; health checks couple to AWS CloudWatch; leaving means re-cutting delegation | LB is Cloudflare-proxied; leaving means traffic path changes | Vendor-neutral — EdgeDNS can be any of 6 providers; delegation is standard NS glue |
At low query volume the dollar difference is trivial. At 100M queries the Route 53 line item is 40 dollars before you count alias-query tricks or second-region checks; Cloudflare's advanced LB pricing (reported as ~$200 for 5 rules at moderate traffic) outruns the base $5 quickly. k8gb's pitch is not that it is cheaper at 1M queries — it is that it never grows a new meter. The bandwidth between your own Hetzner regions is already sunk into the hardware you own.
The rest of this post shows why incubating matters, how the moving parts fit, where the limits are, and what it means for a bex fleet.
Why incubating matters — not just a badge
CNCF maturity levels are not marketing tiers. Each step adds hard requirements that a hobby project cannot clear.
- Sandbox (k8gb: March 30, 2021) — interesting idea, two sponsors, code of conduct.
- Incubating (k8gb: July 18, 2026 vote, August 5 announcement) — proven production adoption plus governance maturity.
- Graduated (k8gb: not yet) — broad adoption, independent security audit, stable API.
To move from Sandbox to Incubating, the TOC demands at least three independent production adopters of adequate scope, plus healthy contributor flow, documented governance, code of conduct, and a security self-assessment with OpenSSF passing checks. Graduated later adds a third-party audit and API stability guarantees.
k8gb's due diligence in January 2026 was led by primary reviewers @TheFoxAtWork and @ricardorocha, with adopter interviews by @angellk and @kgamanji. Technical review by Kashif Khan (TAG Infrastructure, January 30) and governance review by joshgav (January 21) both returned Satisfactory. The project documented security self-assessment and OpenSSF checks. Critically, the TOC verified three adopters across three geographies — two financial-services organizations and one managed-services provider — through direct interviews. The process artifacts are public: level-change issue cncf/toc#2245, PR cncf/toc#2210, and incubation application cncf/toc#1472.
Why it matters for a fleet's control plane — the thing that decides where traffic lands when a region dies — is that the TOC has seen real operators keep it running, and believes the governance will survive a maintainer's job change.
How k8gb actually works — DNS as the control plane
k8gb looks like a DNS tool but behaves like a distributed controller that happens to speak DNS between clusters.
The four components:
-
CoreDNS per cluster, authoritative for the delegated zone. Each cluster runs a CoreDNS instance (exposed via
Service type: LoadBalanceron UDP 53) augmented with thek8gb-io/coredns-crd-plugin. The plugin readsDNSEndpointCRs directly — no zone-file push. A lightweight GeoIP database (geoip.mmdb) enablestype: geoipstrategy where queries from EU get the EU cluster. -
EdgeDNS — the stable parent you already have. Your parent zone
cloud.example.comdelegatesglb.cloud.example.comvia NS records to the CoreDNS LoadBalancer IPs. EdgeDNS can be any provider k8gb supports: AWS Route 53, Cloudflare, Azure DNS, GCP Cloud DNS, NS1, or RFC2136/BIND/Infoblox. In 2025 k8gb migrated Route 53 integration to native external-dns configuration, so it now rides upstreamexternal-dnsrather than calling provider APIs itself. The README lists six providers; the operator adds more as external-dns adds them. -
external-dns — the sync loop. The k8gb controller watches
Gslbhealth (the readiness of pods behind the referenced Ingress/Service), then creates or updates aDNSEndpointCR. external-dns reconciles that CR into EdgeDNS records:gslb-ns-eu-nbg1.cloud.example.com A 65.21.1.10,localtargets-app.glb.cloud.example.com, and TXT heartbeat records peers use to detect split brain. Version 0.20 introduced aZoneDelegationCRD that makes delegation reconciliation deterministic rather than scanning DNS. -
The Gslb controller — the brain. It runs in each cluster, watches the same
Gslbobjects, and decides what the global answer should be. Decisions are shared implicitly: each cluster publishes its view of every Gslb's health into DNS TXT records; peers read those records on next reconciliation and converge.dnsTtlSecondscontrols how long clients and intermediate resolvers cache the answer.splitBrainThresholdSeconds(default 300) is the grace period before a cluster that cannot see peers assumes it should serve traffic anyway.
The four strategies are one field away:
strategy.type | Behavior | When to use |
|---|---|---|
failover (+ primaryGeoTag: eu-nbg1) | Only primary's IP is returned while healthy; on failure, secondary's IP takes over | Active-passive DR; lowest query load |
roundRobin | All healthy clusters' IPs returned as A records (client picks) | Active-active; spreads load without weights |
weighted (roundRobin + weight: {eu-nbg1: 80, eu-fsn1: 20}) | A records ordered/weighted per annotation | Gradual migration, capacity-aware split |
geoip | Client geography (via MaxMind DB) selects nearest healthy cluster | Latency-sensitive global fleet |
Local development is turnkey: make deploy-full-local-setup brings up two k3d clusters, an EdgeDNS BIND container on :1053, CoreDNS on :5053/:5054, and sample Gslb resources — no external DNS provider needed. Known edge cases are fixed but worth knowing: multi-Gslb TTL flip-flop (issue #1837), weighted-RR on RFC2136 (issues #1950, #1964), bare-metal NAT glue publishing private IPs (issue #2360), and health gated on app pods not gateway pods (issue #1754).
What k8gb doesn't do — honest limits
A GSLB that costs nothing extra and needs no hyperscaler is appealing, which is exactly when to list what it is not.
DNS TTL is the RTO floor — not L7 failover. If a client cached the answer 29 seconds before failure, it will not re-query until expiry. Lowering TTL to 5s helps but multiplies queries and still leaves resolver caches and clients that ignore TTL. For WebSockets or gRPC streams you need L4/L7 handoff.
No CDN, no DDoS scrubbing. Route 53 plus CloudFront and Cloudflare LB ship managed edge networks. k8gb returns an IP; where bits flow after that is your problem. Put large static assets behind a CDN; k8gb sits beside it.
Client-side variance is real. Resolver implementations and app-level DNS caches (JVM, Go, Node) mean p50 is not p95. Measure with real clients.
Split brain needs tuning. Two primaries will both answer. splitBrainThresholdSeconds (default 300s) trades faster self-election against longer partition unavailability. Cutting to 60s recovers faster but needs reliable TXT propagation.
GeoIP needs ECS. Not every resolver forwards EDNS Client Subnet; without it k8gb sees the resolver location, not the client.
Operational cost — small but not zero. Each cluster now runs CoreDNS, external-dns, and the k8gb controller. Budget memory, logs, and a runbook for "the DNS answers disagree."
What this means for a bex fleet
A bex fleet is a set of CAPH-provisioned Hetzner clusters reconciled by Cluster API. Management lives in one cluster; workloads spread across the rest. Until now, making that fleet survive the loss of a region meant either accepting the outage or wiring the one global dependency back through AWS: a Route 53 hosted zone holding the failover logic, health checks billed per endpoint, and queries billed per million — the exact hyperscaler coupling a self-hosted platform exists to avoid.
k8gb changes the dependency graph: the global record stays on DNS you already own, and the decision logic lives in the two controllers you already run. The EdgeDNS you delegated to last year when you brought your domain does not gain a new meter. The CAPH MachineDeployments in nbg1 and fsn1 do not need different machine types to unlock GSLB. The Gslb CR above is the only new object a tenant sees.
A plausible bex path looks like:
- Bring your parent zone —
cloud.example.comwherever it already lives (Cloudflare, Route 53, or BIND behind RFC2136). No migration required. - Delegate
glb.cloud.example.comto k8gb-managed CoreDNS IPs via theZoneDelegationCR or one manual NS cut. - Deploy k8gb via Helm on each workload cluster —
clusterGeoTag: eu-nbg1/eu-fsn1,extGslbClustersGeoTags: eu-nbg1,eu-fsn1,dnsZone: glb.cloud.example.com. - Add a
Gslbper app — point it at the Ingress or Gateway you already have. The fleet operator setsdnsTtlSecondscentrally (30s default, 15s for latency-sensitive apps) andprimaryGeoTagper workload. - Monitor delegation, not just pods — alert on
k8gb_healthy_hosts!= expected, EdgeDNS sync lag, and TXT heartbeat staleness beyondsplitBrainThresholdSeconds.
None of this needs a bex-specific CRD today. A single bex.yml per app could later generate the Gslb alongside the App and Domain resources, letting bex deploy create a globally load-balanced hostname as a one-line opt-in — but that is a delivery choice, not a protocol requirement. The protocol is DNS, the CRD is one resource, and the trust signal is the CNCF ladder step it just cleared.
If you run a single Hetzner region, you are now exactly one provider, one datacenter, and one fiber cut away from explaining to your users why every workload you own went dark at once. k8gb does not make multi-region easy — state replication, latency budgets, and data gravity still dominate — but it does make the addressing layer solvable without buying back into the hyperscaler you left. Three production fleets running it at banks and MSPs was enough for the TOC. It may be enough for yours.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.
Sources
- CNCF — k8gb project page: sandbox March 30, 2021, incubating July 18, 2026. cncf.io/projects/k8gb
- DEVOPSdigest — K8gb Accepted as CNCF Incubating Project, August 5, 2026. devopsdigest.com
- CNCF TOC — k8gb incubation application and due-diligence commits (3 adopters, tech/governance satisfactory, OpenSSF passing). github.com/cncf/toc#1472, DD complete 39f49cc, level change #2245
- k8gb repository — Gslb CRD
api/v1beta1, strategiesroundRobin/failover/geoip,dnsTtlSeconds,splitBrainThresholdSeconds. github.com/k8gb-io/k8gb, pkg.go.dev/api - k8gb documentation — EdgeDNS providers, CoreDNS authoritativeness, local playground (
make deploy-full-local-setup). k8gb.io/local, deploy_infoblox - coredns-crd-plugin — DNSEndpoint CRD and GeoIP handling. github.com/k8gb-io/coredns-crd-plugin
- k8gb issues — TTL flip-flop #1837, weighted-RR failures #1950/#1964, NAT glue #2360, gateway health #1754. github.com/k8gb-io/k8gb/issues
- AWS Route 53 pricing — $0.50 hosted zone, $0.50 health check, $0.40 per million queries. AWS Route 53 docs, stormit.cloud
- Cloudflare Load Balancer pricing — $5/mo Pro LB, Advanced tier ~$200 for 5 rules at moderate traffic. GeeksforGeeks pricing
- Civo Academy — CNCF maturity criteria (sandbox → incubating: ≥3 production adopters). civo.com/academy
- CNCF Blog — Multi-cluster fault tolerance with k8gb (Feb 2025) on DNS-based health sharing. cncf.io/blog