Upgrading a production Kubernetes cluster has historically been one of the most nerve-wracking operations in systems engineering. In procedural environments, upgrades rely on imperative automation scripts (e.g., Ansible or SSH loops) executing sequential package manager invocations: upgrading kubeadm, mutating local package versions for kubelet and kubectl, writing new manifests for static control plane pods, and hoping that intermediate failure states do not corrupt the local operating system or orphan cloud resources.
Cluster API replaces procedural, in-place host mutation with declarative, immutable rolling machine replacement.
Rather than modifying binaries on running operating systems, Cluster API reconciles a cluster to a new Kubernetes version by stamping out pristine machines provisioned with pre-baked target-version images, safely validating health and etcd quorum at each step, draining existing workloads respecting PodDisruptionBudgets, and gracefully retiring older instances.
Understanding this process requires examining the fundamental split between management plane and workload cluster upgrades, the strict order of operations enforced by the Kubernetes version skew policy, the internal mechanics of KubeadmControlPlane and MachineDeployment rollouts, and the single-knob workflow enabled by ClusterClass managed topologies.
1. Upgrades as Immutable Machine Replacement
The core architectural philosophy of Cluster API is that compute hosts are disposable and immutable. Mutating packages in place introduces configuration drift: leftover files from previous releases, mismatched kernel dependencies, and uncertain recovery paths if an upgrade script terminates halfway through.
In Cluster API, an upgrade is fundamentally a version-driven rolling replacement:
| Dimension | Procedural In-Place Upgrade (kubeadm via SSH/Ansible) | Declarative CAPI Upgrade (Immutable Replacement) |
|---|---|---|
| Execution Model | Imperative shell commands modifying live hosts | Declarative spec mutation reconciled by controllers |
| Host State | Dirty host: old OS artifacts, stale configs remain | Clean host: pristine VM spun up from versioned image |
| Failure Recovery | Manual troubleshooting of half-upgraded packages | Deterministic rollback or automatic retry of clean VMs |
| Drift Resistance | Prone to divergence across long-lived nodes | Zero host drift: immutable golden images |
| Rollout Cadence | Serial execution requiring custom orchestrators | Governed natively by maxSurge and maxUnavailable |
| Quorum Protection | Operator must manually verify etcd membership | Automated etcd health gates built into KubeadmControlPlane |
2. Two Upgrades That Must Never Be Confused
When operating Cluster API, teams frequently conflate two completely separate upgrade domains:
1. Management Plane Provider Upgrade:
clusterctl upgrade apply --core cluster-api:v1.8.0 ...
-> Upgrades the CAPI controllers running in the management cluster.
-> Does NOT touch workload cluster Kubernetes versions.
2. Workload Cluster Kubernetes Upgrade:
Patching KubeadmControlPlane / MachineDeployment version (or Cluster.spec.topology.version)
-> Upgrades the actual Kubernetes version running on target control-plane and worker nodes.
Management plane controllers must support the target workload cluster version. Always verify your CAPI provider version support matrix before initiating a workload cluster upgrade.
3. The Strict Upgrade Sequence & Version Skew Policy
The Kubernetes project strictly enforces the Version Skew Policy. In any Kubernetes cluster:
kube-apiservermust be upgraded before any worker node components (kubelet,kube-proxy).- Worker nodes may lag the control plane by up to three minor versions ($n-3$), but worker nodes can never be newer than the control plane.
- Direct upgrades must proceed one minor version at a time (e.g., $1.29 \rightarrow 1.30 \rightarrow 1.31$). Skipping minor versions is unsupported because etcd storage schema migrations, API deprecations, and conversion webhooks depend on sequential version progression.
Cluster API enforces this order through its decoupled controller architecture:
flowchart TD
UPGRADE["Operator updates desired version<br/>(e.g., v1.30.0 -> v1.31.0)"] --> KCP_START["1. KubeadmControlPlane Rollout"]
subgraph ControlPlaneRollout["Phase 1: Control Plane Upgrade"]
KCP_SURGE["Surge target-version CP Machine"] --> ETCD_JOIN["Join etcd quorum & verify health"]
ETCD_JOIN --> CP_DRAIN["Cordon & drain oldest CP Machine"]
CP_DRAIN --> ETCD_REMOVE["Remove old CP from etcd member list"]
ETCD_REMOVE --> CP_TERM["Terminate old CP VM"]
CP_REPEAT{"All CP replicas upgraded?"}
CP_TERM --> CP_REPEAT
CP_REPEAT -->|"No"| KCP_SURGE
CP_REPEAT -->|"Yes"| CP_READY["Control Plane Stable & Converged"]
end
KCP_START --> ControlPlaneRollout
CP_READY -->|"Enforces Version Skew: Kubelet <= Kube-apiserver"| MD_START["2. Worker Fleet Rollout"]
subgraph WorkerRollout["Phase 2: Worker Fleet Upgrade"]
MD_NEW_MS["MachineDeployment creates new MachineSet"] --> WORKER_SURGE["Surge worker capacity (maxSurge)"]
WORKER_SURGE --> WORKER_DRAIN["Cordon & drain old worker Machine (PDB compliant)"]
WORKER_DRAIN --> WORKER_TERM["Detach CSI volumes & terminate old VM"]
WORKER_REPEAT{"All worker pools upgraded?"}
WORKER_TERM --> WORKER_REPEAT
WORKER_REPEAT -->|"No"| WORKER_SURGE
WORKER_REPEAT -->|"Yes"| ALL_DONE["Cluster Upgrade Complete"]
end
MD_START --> WorkerRollout
4. Phase 1: Control Plane Upgrade Internals (KubeadmControlPlane)
Upgrading a high-availability (HA) control plane requires maintaining etcd quorum and API availability throughout the entire transition.
The KubeadmControlPlane (KCP) controller manages this lifecycle using an automated surge-and-retire state machine:
sequenceDiagram
autonumber
participant KCP as KCP Controller
participant API as Management API
participant etcd as etcd Quorum
participant OldCP as Old CP Machine (v1.30)
participant NewCP as New CP Machine (v1.31)
KCP->>API: Detects spec.version = v1.31.0
KCP->>etcd: Preflight check: Verify etcd cluster healthy & quorum = 3
KCP->>API: Creates NewCP Machine (v1.31) with new MachineTemplate
NewCP->>etcd: Boots, runs cloud-init, joins etcd quorum (members = 4)
etcd-->>KCP: Quorum confirmed, new API server responding
KCP->>OldCP: Cordon workload node (unschedulable = true)
KCP->>etcd: etcdctl member remove <old-member-id> (members = 3)
KCP->>OldCP: Evict static pods & drain node
KCP->>OldCP: Delete OldCP Machine CR & terminate VM
Note over KCP: Repeats for remaining control-plane machines
The Step-by-Step Control Plane Rollout
- Preflight Health Checks: Before taking any action, KCP confirms that all existing control plane machines are healthy, that the etcd cluster has an active leader, and that no unmanaged members exist.
- Surging New Capacity: KCP clones the infrastructure and bootstrap templates for the target version and creates a 4th control plane
Machine. - Quorum Expansion: The new control plane VM boots, cloud-init executes, and
kubeadm join --control-planeregisters the new node with the existing etcd cluster. The etcd cluster temporarily expands from 3 to 4 members. - Health Verification: KCP verifies that the new machine’s API server is reachable, etcd synchronization is complete, and the
NodereportsReady = True. - Safe Member Retirement: KCP selects the oldest control plane machine. It executes
etcdctl member removeto cleanly shrink the etcd cluster back to 3 members, cordons the node, drains any running workloads, deletes theInfrastructureMachine, and deletes the oldMachineresource. - Convergence: KCP repeats this sequence one machine at a time until all 3 control plane machines are running the target version.
5. Phase 2: Worker Fleet Upgrade Internals (MachineDeployment)
Once the control plane has converged on the target version, worker nodes can be upgraded.
Worker nodes are governed by MachineDeployment resources. As explored in worker hierarchy internals, infrastructure and bootstrap templates in Cluster API are immutable. You cannot edit an existing AWSMachineTemplate or KubeadmConfigTemplate to point to a new version.
Instead, the upgrade follows an immutable rotation protocol:
1. Create new InfrastructureMachineTemplate (e.g. prod-worker-v1-31-amt) with target image/AMI.
2. Patch MachineDeployment.spec.template:
- spec.template.spec.version = "v1.31.0"
- spec.template.spec.infrastructureRef.name = "prod-worker-v1-31-amt"
3. MachineDeployment detects template drift, derives a new hash, and creates a new MachineSet.
4. MachineDeployment shifts capacity between old and new MachineSets under RollingUpdate rules.
Rollout Capacity Controls: maxSurge and maxUnavailable
The worker rollout rate is governed by MachineDeployment.spec.strategy.rollingUpdate:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
maxSurge: 1: Cluster API creates 1 new worker machine before terminating an old one. This preserves total cluster compute capacity, ensuring workloads have headroom to reschedule before old nodes are drained.maxUnavailable: 0: Ensures that old worker nodes are never cordoned or drained until replacement nodes are verified as running and fully ready (minReadySecondssatisfied).
Graceful Node Drain During Upgrade
When an old worker machine is scheduled for retirement, Cluster API invokes the Kubernetes Eviction API:
- Respects all active
PodDisruptionBudgets(PDBs). - Waits for attached CSI persistent volumes to cleanly unmount and detach.
- Honors
spec.template.spec.nodeDrainTimeoutto prevent stuck pods from stalling the cluster-wide upgrade.
6. Single-Knob Upgrades with Managed Topologies (ClusterClass)
In traditional Cluster API configurations, an operator must patch the KubeadmControlPlane, create new infrastructure templates, and patch each MachineDeployment individually.
With ClusterClass and Managed Topologies, the entire cluster upgrade collapses into modifying a single field on the root Cluster resource:
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: prod-cluster
spec:
topology:
class: production-cluster-class
version: v1.31.0 # <- The single source of truth for cluster version
controlPlane:
replicas: 3
workers:
machineDeployments:
- class: default-worker
name: md-0
replicas: 10
How the Topology Controller Drives the Upgrade
When you patch spec.topology.version:
- The Topology Controller intercepts the change.
- It automatically generates new versioned template objects for the control plane and workers from the underlying
ClusterClass. - It updates
KubeadmControlPlane.spec.versionfirst, initiating the control plane rolling replacement. - It watches KCP conditions. While the control plane is upgrading, the topology controller deliberately holds worker
MachineDeploymentsat the previous version to strictly enforce the version skew policy. - Once KCP reaches
status.version = v1.31.0and all control plane replicas are available, the topology controller cascades the version update to all workerMachineDeployments.
Critical Rule: When using managed topologies, never directly edit generated
KubeadmControlPlaneorMachineDeploymentresources. Any manual changes made to child objects will be automatically overwritten by the topology reconciliation loop.
7. Troubleshooting Stalled Upgrades
Upgrades can stall if underlying infrastructure, cloud quotas, or application constraints prevent controllers from making forward progress.
flowchart TD
START["Upgrade Stalled"] --> PHASE{"Which Phase is Blocked?"}
PHASE -->|"Control Plane"| CP_CHECK{"Where is KCP stuck?"}
CP_CHECK -->|"Surge VM Pending"| CP_Q1["Check cloud quotas, subnet IPs,<br/>and new image AMI/VHD availability"]
CP_CHECK -->|"etcd Not Joining"| CP_Q2["Inspect cloud-init logs & TLS certs<br/>for port 2379/2380 connectivity"]
CP_CHECK -->|"Old CP Won't Delete"| CP_Q3["Verify etcd quorum: KCP will not remove<br/>member if remaining members < quorum"]
PHASE -->|"Worker Fleet"| W_CHECK{"Where is MachineDeployment stuck?"}
W_CHECK -->|"Surge Capacity Blocked"| W_Q1["Check maxSurge quota & cloud instance limits"]
W_Q2["Check PDB violations blocking pod eviction<br/>or CSI volume detach locks"]
W_CHECK -->|"Drain Hanging"| W_Q2
W_CHECK -->|"Template Hash Drift"| W_Q3["Ensure new MachineTemplate created instead<br/>of mutating old template in place"]
Step-by-Step Diagnostic Inspection
1. Inspect Overall Rollout State
clusterctl describe cluster <cluster-name> --show-conditions
This command provides a real-time hierarchical view of the cluster tree, highlighting which machine is currently surging or draining and showing failure reasons.
2. Check Control Plane Rollout Status
kubectl describe kubeadmcontrolplane <cluster-name>-control-plane
Look for:
ReadyReplicasvs.UpdatedReplicas.- Condition
MachinesCreated: Checks if cloud instances are being launched. - Condition
Resized: Verifies whether KCP is surging new members or waiting for old member deletion.
3. Diagnose Worker Fleet Drain Failures
If the worker rollout is frozen with an old machine stuck in Deleting:
kubectl describe machine <stuck-worker-machine>
Look at the events for:
DrainNodeFailed: Indicates a pod cannot be evicted due to a restrictivePodDisruptionBudget(minAvailable: 100%) or a pod refusing to terminate within its grace period.VolumeDetachFailed: Cloud storage provider has not released an attached volume.
8. Practical Upgrade Checklist
Pre-Upgrade Verification
[ ] Management cluster CAPI providers support the target Kubernetes version.
[ ] Target OS machine image (AMI / VHD / template) built, published, and accessible.
[ ] Workload cluster etcd cluster is healthy and all control plane machines are Ready.
[ ] Cloud provider quota verified: account has headroom for maxSurge instances.
[ ] Subnet CIDRs verified: sufficient IP addresses available for surge VMs.
Execution (Manual or Managed Topology)
[ ] Upgrade control plane first: wait for KCP to report all replicas updated and healthy.
[ ] Verify workload cluster API server health via kubectl.
[ ] Upgrade worker pools: rotate MachineDeployment templates and update versions.
[ ] Monitor node drains and confirm workloads reschedule cleanly.
Post-Upgrade Verification
[ ] kubectl get nodes reports all control plane and worker nodes on target version.
[ ] No orphaned Machine resources remain in the management cluster.
[ ] Storage CSI and Networking CNI plugins functioning normally on new nodes.
9. Key Takeaways
- Immutable Replacement over In-Place Mutation: Cluster API never runs package upgrade scripts on running nodes. Upgrades execute via rolling machine replacement using pre-baked golden images.
- Strict Version Skew Sequencing: The control plane must always upgrade and stabilize before worker nodes are touched.
- Etcd Quorum Preservation:
KubeadmControlPlanemanages control plane rolling updates by temporarily expanding etcd quorum from 3 to 4, verifying health, and cleanly removing the old member. - Managed Topologies Simplify Upgrades: With
ClusterClass, changingCluster.spec.topology.versionautomatically orchestrates the entire two-phase rollout while maintaining version skew compliance. - Surge Headroom Matters: Ensure cloud instance quotas and network subnet IP address pools have sufficient headroom to accommodate
maxSurgecapacity during fleet rollouts.
Next in the series
Now that we understand how Cluster API orchestrates rolling upgrades across control planes and worker fleets, explore Part 11: ClusterClass and Managed Topologies to learn how platform teams standardize multi-cluster blueprints using reusable topologies and inline patches.