Skip to main content

kube-hetzner vs CAPH: What Your First Hetzner Provisioning Choice Costs by the Second Cluster

10 min readDora NodaDora Noda
Share
On this page

On June 15, 2026, Hetzner deprecated the cx22 server type millions of tutorials were written against, and the API started rejecting new provisions on it. Every Hetzner Kubernetes fleet had the same afternoon: find every reference to a dead server type and move. How painful that afternoon was depended almost entirely on a decision made months earlier — whether the fleet was provisioned with Terraform or with Cluster API. That is what this post prices out.

The answer up front

Here is the honest split, with the receipts below: Terraform (kube-hetzner) wins the first cluster a human drives. Cluster API (CAPH) wins the moment fleet membership itself needs an API instead of a workstation. Neither tool is the cheap option in the other's territory, and the bill for choosing wrong arrives with the second cluster — not the first.

Dimensionkube-hetzner (Terraform)CAPH (Cluster API)
Day-1 setupOne module, one apply, running k3s cluster in minutesManagement cluster first, then Cluster objects; hours, not minutes
Kubernetes upgradesBump a variable, re-apply; k3s/MicroOS auto-upgrades do the restEdit a template field; rolling update with version-skew guardrails
Dead-node replacementAutoscaler restores capacity; operator re-applies to converge stateMachineHealthCheck detects and remediates automatically
Server-type churn (cx22 → cx23)Edit variable, apply per cluster, verify each by handEdit MachineDeployment template; controllers roll every cluster
Second, third, Nth clusterNew state root per cluster; cost scales linearly with humansNew Cluster object on the existing manager; near-constant marginal cost
Driven byA workstation running TerraformThe Kubernetes API — GitOps, scripts, agents
Total: cost of the second clusterRepeat most of day-1 plus day-2 wiring, by hand, againOne more manifest against infrastructure you already run

The delta is the whole story: Terraform's per-cluster cost stays roughly flat per cluster because a human repeats the work, while CAPI amortizes its higher setup cost across every cluster after the first. If you will only ever run one cluster, the first row of that table is the only one that matters. If you will run two, read on.

What each tool actually provisions

kube-hetzner (kube-hetzner/terraform-hcloud-kube-hetzner) is a Terraform module that provisions k3s clusters on Hetzner Cloud via Terraform plus cloud-init. Out of the box you get k3s on MicroOS, the Hetzner Cloud Controller Manager and CSI driver pre-configured, and a Hetzner load balancer fronting Traefik or Nginx with Proxy Protocol.

The defaults are opinionated in a good way: HA with three control-plane nodes and two agents, cluster-autoscaler integration, and automatic upgrades to the latest MicroOS and k3s — the headline promise is "maintenance-free." It supports any Hetzner Cloud instance type including the cheaper CAX ARM machines, uses the private network throughout, and offers optional WireGuard encryption. It is actively maintained through 2026.

CAPH (syself/cluster-api-provider-hetzner) is the Cluster API infrastructure provider for Hetzner, maintained independently by Syself and the community — not an official Hetzner project. It extends the Kubernetes API with CRDs so clusters on Hetzner Cloud and Hetzner bare metal are declared as Cluster, MachineDeployment, and Machine objects and reconciled by controllers.

Upgrades are template edits rolled out by MachineDeployments, health is enforced by MachineHealthCheck remediation, and the same manifests work alongside Talos bootstrap and control-plane providers. The price of admission is a management cluster (often bootstrapped from kind) that hosts the controllers — you run Kubernetes to run Kubernetes.

Note the asymmetry hidden in those two paragraphs: kube-hetzner's state lives in Terraform state files on someone's workstation or in remote state; CAPH's state lives in etcd behind the Kubernetes API. Every day-2 difference below is a consequence of that one fact.

Day-2 operations, side by side

Upgrades: re-apply vs reconcile

On kube-hetzner, a Kubernetes upgrade is a variable bump and terraform apply. The module leans on k3s and MicroOS auto-upgrades to carry nodes forward, which is genuinely low-touch for the happy path — but the rollout is driven by the apply run, watched by whoever ran it, and its safety properties are whatever the module author scripted. If the apply fails halfway, resuming it is a human decision informed by Terraform's plan diff.

On CAPH, a Kubernetes upgrade is an edit to a MachineDeployment template (or a Cluster topology version), and the rollout is executed by controllers with built-in guardrails — CAPI refuses to upgrade worker machines ahead of the control plane, enforcing version-skew policy mechanically rather than by runbook discipline. The rollout's progress is itself API state (replicas, ready, updated, unavailable), so watching it is kubectl get machinedeployment, not tailing an apply log. Upgrades become "edit a field," as one operator put it, with the machine-readable rollout status that GitOps and automation need.

For one cluster, both are fine and Terraform is arguably simpler. For five clusters, the Terraform operator runs five applies and watches five logs; the CAPI operator edits five objects (or one template they all render from) and the controllers do the watching.

Node replacement: converge vs remediate

A node dies at 3am. On kube-hetzner, cluster-autoscaler notices the capacity shortfall and provisions a replacement via the Hetzner API — capacity self-heals. But Terraform state still describes the old node, so an operator re-applies to converge state with reality, and any per-node customization the module doesn't own needs a human to confirm it landed. The loop closes through a person.

On CAPH, MachineHealthCheck notices the NotReady node (or a node that never joins within nodeStartupTimeout), deletes the Machine, and the MachineDeployment controller provisions a replacement to satisfy the replica count — no human in the loop at all. Remediation is a controller reconciliation, not an incident response. What still pages a human is different in kind: controller failures, provider outages, or a whole class of machines failing to come up (which no per-node remediation can fix).

This is the clearest illustration of the state asymmetry. Terraform converges desired state when a human runs it; CAPI converges desired state continuously, including at 3am. A 2026 Hetzner Kubernetes guide put the Terraform side plainly: initial setup is automated, but day-2 operations remain your responsibility. With CAPI, day-2 operations remain your controllers' responsibility — which is only better if someone operates the controllers.

Case study: the June 2026 cx22-to-cx23 churn

June 15, 2026 is the worked example this comparison needed. Hetzner raised prices and rotated server generations simultaneously: cx22, cx32, and cx42 stopped being orderable — the API rejects provisioning with "server type N is deprecated" — replaced by cx23, cx33, and cx43 at the same 2 vCPU / 4 GB / 40 GB shape. A price-tracking project documented the fallout: every page still quoting cx22 named a plan you cannot buy at a price that no longer applies. And Hetzner blocks rescaling a server onto a plan with less disk than it already has, so this was replace-not-resize for everyone: new nodes had to be provisioned and old ones drained.

The Terraform workflow: find every server_type variable referencing a deprecated type (across every state root, i.e., every cluster), update each, apply each, and verify each cluster converged — with the deprecated-type API error as your linter if you missed one. Operators who pinned types per pool did a careful multi-apply dance; operators who parameterized well did one edit per cluster. Either way, the unit of work was the cluster, and the driver was a human at a workstation.

The CAPI workflow: edit the server-type field in the MachineDeployment templates (or the ClusterClass they render from) and let the controllers perform rolling replacements across every cluster whose template changed. Same replace-not-resize physics — CAPI can't resize Hetzner disks either — but the unit of work was the template, and the driver was reconciliation. One edit could, in principle, roll the entire fleet while the operator watched kubectl instead of driving applies.

Neither side escaped the afternoon. But the Terraform afternoon scaled with cluster count and the CAPI afternoon scaled with template count — and fleets accumulate clusters faster than they accumulate templates.

The flip point: when the fleet needs an API

So when does the answer change? Precisely when fleet membership itself needs to be programmable:

  • The second cluster. Not because two is magical, but because it's the first time you pay Terraform's per-cluster cost twice — second state root, second apply pipeline, second snowflake risk — versus CAPI's second manifest.
  • GitOps-driven fleet operations. A Cluster object can be committed, reviewed, and reverted like any workload manifest; a Terraform state transition can be reviewed as a plan but executes as an imperative run. Fleet-wide rollbacks are a git revert in one world and a reverse-migration project in the other.
  • Programmatic provisioning. Autoscaling the fleet (not the nodes within one cluster), spinning up preview environments per pull request, or letting an AI agent provision and decommission clusters — all of these need machine-readable fleet state behind an API. Terraform behind a wrapper script is an API with one client and no concurrency story; the Kubernetes API is the concurrency story.

And the cost of switching late is concrete: there is no in-place conversion from a kube-hetzner cluster to a CAPI-managed one. Migration means provisioning the replacement cluster under the new tool and moving workloads — a re-platforming, not a refactor. That makes the first provisioning choice load-bearing in a way most day-1 decisions aren't: you're not just picking how to create a cluster, you're picking where your fleet's source of truth will live for every cluster after it.

Which to pick, honestly

Pick kube-hetzner if: you run one cluster (or a fixed handful), a human operator drives changes, you want k3s's small footprint and MicroOS auto-upgrades, and you'd rather delete a management cluster you don't have than operate one. This is the right answer for most side projects, single-product startups, and first clusters generally — including first clusters that later migrate once they know their shape.

Pick CAPH if: you already see the second cluster coming, you want fleet operations in GitOps, you need bare metal under the same API as cloud, or anything non-human — scripts, controllers, agents — will ever provision clusters. Accept the management cluster as the cost of admission and operate it like the production system it is.

The deeper trend favors the API side: NVIDIA's DRA driver donation, the KAI scheduler, Headlamp's Cluster API plugin — the ecosystem is converging on machine-readable infrastructure state as the default interface, not the power-user option. The workstation-driven fleet isn't wrong; it's just priced for a fleet of one. Count your clusters honestly — including the ones you haven't created yet — and choose accordingly.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with fleet operations behind an API your agents can drive. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex