Skip to main content

Cluster API v1.12: What Chained Upgrades Actually Save a Kubernetes Fleet That Fell Behind

10 min readDora NodaDora Noda
Share
On this page

Say a Cluster API-managed fleet on Hetzner bare metal drifted three Kubernetes minor versions behind — not an edge case, just a team that skipped a few upgrade cycles because each one meant babysitting a rolling replace. Before Cluster API (CAPI) v1.12, catching up meant three separate rolling replacements, run one at a time, each cordoning and draining every node, provisioning a new machine, bootstrapping it, and joining it back to the cluster. Three reconciliation passes, three provisioning cycles per node, three maintenance windows.

CAPI v1.12, released in January 2026, collapses that into one operation. Chained upgrades let an operator declare a target version and have Cluster API compute and execute the whole multi-minor-version plan itself, skipping intermediate versions on worker nodes wherever the Kubernetes version skew policy allows it. In-place updates, the release's other headline feature, go further for changes that don't need a replace at all — mutating a Machine's spec directly instead of deleting and recreating the node.

For a hosted PaaS on a hyperscaler's managed node pools, a node "replace" is closer to swapping a pre-baked AMI — fast, and mostly invisible. For a Cluster API Provider Hetzner (CAPH) fleet running on owned bare metal, a replace means Hetzner actually provisioning a physical or dedicated-vCPU machine from scratch: no snapshot to boot from, an OS install, then kubeadm bootstrap and join. That cost was always there under the old model — v1.12 is the first CAPI release that lets an operator avoid paying it more times than necessary.

What Actually Shipped in v1.12

Both features work off the same core mechanism: when a Machine or Cluster spec changes, Cluster API no longer assumes the only tool available is an immutable rollout. It now chooses between a full replace and an update extension — a Runtime SDK hook a platform team registers to handle specific kinds of in-place changes.

The mechanism runs through three hooks, in sequence:

  1. CanUpdateMachine — called by the KubeadmControlPlane controller to ask whether a control-plane machine's pending changes can be applied without replacing it.
  2. CanUpdateMachineSet — the equivalent check the MachineDeployment controller runs against every machine in a MachineSet before treating the whole set as eligible for in-place handling.
  3. UpdateMachine — the hook that actually applies the change once eligibility is confirmed.

The scope of what's eligible is narrow and explicit: extensions can only patch spec fields on the Machine, InfraMachine, or BootstrapConfig (or spec.template.spec on the templated equivalents for a MachineSet). Status fields are never in play. If a batch of changes includes even one field an extension can't cover, Cluster API falls back to its old behavior — delete, provision, bootstrap, join — for that machine. Rolling worker upgrades using the OnDelete strategy skip in-place handling entirely; the feature only activates when the InPlaceUpdates feature flag is on, and — a real limitation worth flagging rather than glossing over — the current implementation allows only one registered extension per hook across the whole management cluster.

The documented example CAPI's own maintainers reach for is credential rotation: rewriting a machine's user credentials doesn't require restarting pods, so it's a clean candidate for a hook to handle in-place instead of triggering a full node replacement. A kernel parameter tweak or an OS-level config change that doesn't touch running workloads is the same shape of problem.

Chained upgrades solve a different, additive problem: getting from a stale control-plane version to a current one without manually stepping through every intermediate minor release. An operator sets a target version — say, going from v1.32 to v1.35 — and Cluster API computes an upgrade plan itself: control-plane machines upgrade first, one minor version reconciliation at a time (control-plane version skew rules don't allow skipping), then worker machines follow and are allowed to skip straight past intermediate minors whenever the Kubernetes version skew policy permits it. Upgrade plan runtime extensions let a platform customize how that sequencing runs, and lifecycle hooks can fire auxiliary tasks — like an addon upgrade — after the control plane moves.

The Reconciliation-Pass Math on a Bare-Metal Fleet

Here's what that actually nets out to for a fleet that's fallen three minor versions behind, expressed as reconciliation passes rather than abstract "faster" language:

Before v1.12With v1.12 chained upgrades
Control-plane upgrade operations3 separate rolling replacements (v1.32→33, 33→34, 34→35)1 operation, computed and executed as a plan; control-plane still steps through each minor (skew rules require it) but with no manual re-triggering between steps
Worker upgrade operations3 separate MachineDeployment rollouts, one per minor versionAs few as 1, if skew policy allows workers to jump straight to the target version
Node provisioning cycles (CAPH bare metal)Up to 3x per node — full Hetzner provisioning, kubeadm bootstrap, and join on every intermediate hopAs low as 1x per node for the final state
Operator-triggered maintenance windows3, spaced out however long the team could tolerate each rolling replace1

The reason this matters more on CAPH than on a managed node pool isn't the Kubernetes control plane logic — that's identical either way. It's what a "node provisioning cycle" actually costs underneath it. A hyperscaler's managed node pool replaces a node by booting a new instance from a pre-baked, versioned image; the marginal cost of doing that three times instead of once is mostly wall-clock, not operator attention. A CAPH-managed Hetzner node doesn't have that shortcut — Hetzner Robot provisions dedicated hardware or a fresh cloud instance from scratch each time, no snapshot fast-path. Multiplying that by three for a fleet that skipped a few upgrade windows was always the honest cost of owning the hardware instead of renting a hyperscaler's abstraction over it. Chained upgrades doesn't remove that cost — it removes the multiplication.

Walk one worker MachineDeployment through both paths to see where the savings actually land. Under the old, pre-v1.12 model, going from v1.32 to v1.35 meant three separate MachineDeployment rollouts. Each one cordons the deployment's nodes, drains them (bounded by however aggressive the fleet's PodDisruptionBudgets and termination grace periods are, but rarely a fast operation on anything running stateful workloads), provisions a replacement machine on Hetzner from scratch, bootstraps kubeadm, and joins it back before the deployment is considered healthy again — then repeats for v1.33→v1.34, then v1.34→v1.35. Three cordon-drain-provision-bootstrap cycles per node, three separate windows where the operator has to watch the rollout land cleanly before triggering the next one. With chained upgrades, the operator sets the target once at v1.35. If the worker version skew policy allows workers to sit up to three minor versions behind the control plane — which, per Kubernetes' own skew policy, it typically does — the worker MachineDeployment can go straight from v1.32 to v1.35 in a single rollout: one cordon-drain-provision-bootstrap cycle per node instead of three, and one window to watch instead of three.

Before Rolling This Into Production

Both features are additive to the existing rollout model, not a replacement for it, which makes the honest adoption question less "is this safe" and more "what still needs verifying before trusting it against a fleet that's actually behind." A few things worth confirming in a CAPD (local Docker) or staging CAPH environment before pointing chained upgrades at a production fleet three minors behind:

  • Addon compatibility across every intermediate minor, not just the target. A chained upgrade skips Cluster API's replacement cycles for intermediate versions, but CNI, CSI driver, and ingress controller compatibility still has to hold across the full version range being crossed — Cluster API isn't validating that an addon built against v1.32 APIs still functions once the control plane lands on v1.35.
  • Whatever upgrade-plan or lifecycle-hook logic a platform layers on top gets exercised differently by a chained plan than by three independent rollouts — a hook written and tested against single-minor-version upgrades is worth re-running against a multi-hop plan before it's the thing gating a production fleet's catch-up.
  • Rollback shape changes. A failed single-minor rollout under the old model has one previous known-good state to roll back to. A failed chained upgrade partway through a multi-minor plan means reasoning about which intermediate state the fleet actually landed in — worth rehearsing once against a disposable cluster rather than discovering it live.

Where In-Place Updates Still Owe You the Same Discipline

The temptation with "in-place" is to read it as "safe to ignore." It isn't, and Cluster API's own docs are explicit about why: a machine undergoing an in-place update is treated as unavailable, on the reasoning that any in-place change is potentially disruptive even when it doesn't require a pod restart. That means the same PodDisruptionBudget math, the same surge/maxUnavailable planning that governs a rolling replace, still has to hold for in-place updates — a platform can't treat them as a zero-coordination change just because the node itself doesn't get torn down.

Two things follow from that for a CAPH-managed fleet:

  • The eligibility boundary is doing real work. Because only spec fields on the Machine, InfraMachine, and BootstrapConfig are in scope, most changes that actually matter to a node's runtime state — new kubelet config that needs a restart, a CNI upgrade, anything touching what's already scheduled — are still going to fall through to a full replace. In-place updates shrink the set of changes that pay the replace tax; they don't eliminate node replacement as a discipline a platform has to keep working.
  • One extension per hook is a real ceiling, not a rounding error. A platform team that wants both credential-rotation handling and, say, a custom OS-patch-in-place workflow can't just register two independent extensions on UpdateMachine today — that logic has to live behind a single extension that internally branches. Worth designing for from day one rather than discovering at the second use case.

What This Means for a Fleet Building on CAPH Today

The features are genuinely useful and directly aimed at the operational pain of running owned hardware instead of a hyperscaler's node pools — but they shipped as experimental, gated behind InPlaceUpdates, with a single-extension-per-hook ceiling that reads like a v1 API surface still finding its shape. The sane adoption path for a fleet on bex's Cluster API-based platform is chained upgrades first — it needs no custom extension code, works against the existing skew-policy-aware default behavior, and directly collapses the exact multi-pass cost a fleet that's fallen behind is paying today. In-place update extensions are worth prototyping against a narrow, well-understood case like credential rotation, but the one-extension-per-hook limit and the "still counts as unavailable" semantics mean it's not yet the place to bet a fleet's entire node-lifecycle story.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, provisioned and upgraded through the same Cluster API machinery this release improves. Star the repo on GitHub or deploy your first app today.

Sources

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