Tools · Cluster API

Cluster API

Cluster API (CAPI) turns entire Kubernetes clusters into ordinary declarative API objects: you write a Cluster and a MachineDeployment the way you write a Deployment, and controllers on one “management” cluster create, scale, upgrade, repair and eventually delete the real thing. It solves the problem that ruins every growing organisation — clusters built by hand or clicked into a console, each subtly different, each terrifying to upgrade, none reproducible — by turning clusters from pets into cattle, and “build me a cluster in Frankfurt” into a reviewable pull request.

☺ Explain it like I’m 10

Your school needs lots of identical treehouses. Right now every one is built by a different kid from memory, so no two have the same ladder and nobody dares fix the roof. Cluster API is a workshop with a blueprint drawer and a team of robots. Drop in a card saying “treehouse, 3 rooms, version 31” — the robots build it, check on it daily, and swap in a new plank when one rots. Want version 32? Cross out 31, write 32, and the robots quietly build new rooms and take the old ones away, one at a time, while everyone keeps playing. You never climb a ladder with a hammer again — you edit a card.

🦉🦫Your hosts for this topic: Professor Owl & Benny the Beaver — Owl explains why a cluster should be an API object at all and what that buys a fleet, while Benny wires up the actual machinery: the management cluster, the providers, the templates, and the version bump that rolls fifty clusters without a maintenance window.

What Cluster API is and the problem it solves

☺ Like you’re 10: It’s Kubernetes used to build more Kubernetes — one cluster whose job is making and looking after all the other clusters.

Cluster API is a Kubernetes SIG Cluster Lifecycle sub-project — not a CLI that builds a cluster and walks away, but a set of controllers plus a family of custom resources. Install those controllers into a cluster (the management cluster) and from then on “create a cluster” means kubectl apply, because a cluster’s desired state is now YAML a controller reconciles forever. That single move drags provisioning into the same world as everything else here: GitOps can apply it, admission policy can vet it, RBAC can scope it, kubectl get can report on it.

The problem before CAPI

Before CAPI, cluster creation was solved twice and badly. The console path produced snowflakes: someone clicked through EKS or AKS, picked slightly different node sizes each time, and the knowledge lived in a wiki page that went stale in a fortnight. The scripted path — Terraform, Ansible, home-grown bash — was better but still run-to-completion: it made the world correct at 14:02 on Tuesday and then stopped caring. Nothing noticed a drifted node pool; nothing rebuilt a dead machine; upgrades were sweaty bespoke events repeated once per cluster. At three clusters that is annoying. At forty it is the entire job.

Clusters as cattle

CAPI applies the oldest lesson in cloud native — cattle, not pets — one level up the stack. A node is never patched in place; it is replaced. A cluster is never hand-tuned; it is re-derived from a template. Because the controllers reconcile continuously, drift is corrected without a human, a dead machine is rebuilt automatically, and an upgrade is a number you change in Git. The abstraction you trust for pods — Deployment owning ReplicaSets owning Pods — reappears as MachineDeploymentMachineSetMachine. That symmetry is the design thesis.

◆ Key idea

CAPI does not invent a new provisioning language. It reuses the Kubernetes API machinery — CRDs, controllers, owner references, conditions, level-triggered reconciliation — and points it at machines and clusters. Everything you learned on Platform APIs & Operators applies unchanged. A cluster is just another custom resource with a controller behind it.

What Cluster API is deliberately not

Half of all CAPI confusion comes from expecting too much. CAPI creates a cluster that is empty and correct: a working control plane, joined nodes, a kubeconfig. It does not install a CNI, so a new cluster’s nodes sit NotReady until you give it one; nor ingress, cert-manager, the mesh or monitoring agents; nor does it back up etcd, which is Velero’s job. Filling a fresh cluster belongs to GitOps — knowing where that line sits saves hours of “why is my cluster broken?”

Where it fits in a platform

☺ Like you’re 10: CAPI sits at the very bottom — it makes the ground that everything else stands on.

In the layered platform model, Cluster API sits underneath everything: it produces the Kubernetes substrate itself. Every other tool on this site assumes a cluster exists; CAPI is how the cluster came to exist, and how it will be upgraded and eventually retired. In the reference architecture it lives on the hub of a hub-and-spoke fleet, alongside the fleet GitOps controllers.

Its neighbours

Beside CAPI sits Crossplane: Crossplane makes cloud resources into Kubernetes objects, CAPI makes clusters into Kubernetes objects, and mature platforms run both. Above it, Argo CD or Flux applies the CAPI manifests from Git so “add a region” is a merge, then installs add-ons once the new kubeconfig appears. Underneath, CLI IaC — Terraform or OpenTofu builds the accounts, the network backbone and the very first management cluster, since you cannot run CAPI before you have a cluster. Inside each workload cluster, Karpenter or Cluster Autoscaler handles minute-to-minute node scaling while CAPI owns the pool’s shape and version.

Why platform teams reach for it

Uniformity: one template stamps every cluster, so there are no snowflakes and any cluster can be rebuilt from Git. Upgrades: changing a version field is dramatically cheaper than forty artisanal maintenance windows, and it is rollback-able. Portability: the same object shape works on AWS, Azure, GCP, vSphere, OpenStack and bare metal with only the provider templates swapped — which is what makes a hybrid or on-prem platform tractable. The self-service payoff arrives when a team opens a PR adding a directory and receives a whole environment.

CNPE domain relevance

Be clear-eyed: Cluster API is not on the official CNPE tool list, so no task will ask you to type clusterctl. It is here because it is the industry standard for declarative cluster lifecycle, and because the concepts are squarely examinable — infrastructure as code, control-plane reconciliation, immutable infrastructure, fleet management, the management-versus-workload split. Expect those under Platform Architecture & Infrastructure (15% of the exam). The primary lesson here is Multi-Cluster & Fleet; the philosophy is on IaC & Control Planes; the blueprint is on the exam guide.

How it works — architecture, components and CRDs

☺ Like you’re 10: One boss cluster holds the blueprints and the robots. Each robot knows one job: make VMs, turn a VM into a node, or look after the control plane.

CAPI splits into a cloud-agnostic core and pluggable providers. Core understands Cluster, Machine, MachineSet, MachineDeployment, MachineHealthCheck and MachinePool, but knows nothing about EC2 or cloud-init; anything cloud-specific is delegated across a versioned contract to a provider that owns its own CRDs.

Management cluster vs workload clusters

The management cluster runs the CAPI controllers and stores the objects describing your fleet. Keep it small, hardened, HA, and free of business workloads — it can create and destroy every cluster you own and holds credentials to all of them, making it the highest-value target on the platform. A workload cluster is one it provisioned; it runs your applications and ideally knows nothing about CAPI. Crucially the hub must not be a runtime dependency of the spokes: if it is down, every workload cluster keeps serving traffic — you just cannot make fleet-level changes until it returns.

The management cluster usually starts as a temporary local kind cluster: install CAPI there, build the real management cluster with it, then pivot — move the CAPI objects onto the new cluster so it manages itself. That is what clusterctl move is for.

The three provider roles

Three provider types compose to make a node. Keeping them straight is the single most useful piece of CAPI knowledge.

Provider roleResponsibilityExamplesCRDs it owns
InfrastructureCreates the real substrate — VMs, networks, security groups, the control-plane load balancerCAPA (AWS), CAPZ (Azure), CAPG (GCP), CAPV (vSphere), CAPO (OpenStack), Metal3 (bare metal), CAPD (Docker — for local testing)AWSCluster, AWSMachineTemplate, DockerCluster, DockerMachineTemplate
BootstrapTurns a blank VM into a Kubernetes node — generates the cloud-init / ignition that runs kubeadm init or kubeadm joinKubeadm (CABPK, the default), Talos, RKE2, MicroK8sKubeadmConfig, KubeadmConfigTemplate
Control planeOwns the API-server/etcd tier as a unit: HA member management, certificate handling, rolling upgradesKubeadmControlPlane (KCP, the default), plus managed variants like AWSManagedControlPlane (EKS) or AzureManagedControlPlane (AKS)KubeadmControlPlane, KubeadmControlPlaneTemplate

Two more categories exist: IPAM providers (addresses on on-prem networks) and add-on providers such as CAAPH, the Cluster API Add-on Provider for Helm. Core CAPI also ships ClusterResourceSet — a label-selected bundle of ConfigMaps/Secrets applied to matching new clusters, the classic way to get a CNI onto a cluster the moment it is born.

The object family, and why it looks familiar

The worker-node hierarchy deliberately mirrors the workload hierarchy you know.

Workloads (pods)CAPI (machines)What it does
DeploymentMachineDeploymentDeclares a worker pool; owns MachineSets; a change to version or the template triggers a rolling replacement
ReplicaSetMachineSetKeeps N Machines of one exact revision alive; you rarely touch it directly
PodMachineOne node. Immutable: to change it you delete and recreate it
ClusterThe top-level object; points at an infrastructure object and a control-plane object via infrastructureRef and controlPlaneRef
liveness probe + restartMachineHealthCheckWatches Node conditions; deletes (and therefore replaces) Machines that stay unhealthy too long
node group / ASGMachinePoolDelegates the pool to a cloud-native group (ASG, VMSS, MIG) instead of managing individual Machines
a Helm chart of the appClusterClassA reusable cluster template; Clusters written with spec.topology are stamped from it
Cluster cluster.x-k8s.io AWSCluster infrastructure provider KubeadmControlPlane control-plane provider infrastructureRef controlPlaneRef Machine × 3 api-server + etcd nodes MachineDeployment a worker pool MachineSet Machine KubeadmConfig bootstrap → cloud-init AWSMachine infra → EC2 instance Node joins the workload cluster every Machine = bootstrap + infra Machines are immutable — replaced, never patched

ClusterClass and managed topologies

Writing six coupled objects per cluster does not scale. ClusterClass (a managed topology) is the answer: the platform team authors one ClusterClass holding the control-plane template, the infrastructure template and named worker classes, optionally with typed variables and patches. A consumer writes a tiny Cluster with a spec.topology block — class, version, replica counts — and the topology controller expands it into the full object graph and keeps it reconciled. Changing the golden image fleet-wide becomes one edit. It is the difference between handing developers a form and handing them a blank YAML file, and it needs the CLUSTER_TOPOLOGY feature enabled at clusterctl init time.

The resources you will actually write

☺ Like you’re 10: Here are the real cards you drop in the blueprint drawer.

Four manifests carry almost all of CAPI: the cluster, the control plane, a worker pool and a health check — then, once the fleet grows, the ClusterClass that replaces the first three.

A cluster and its control plane

The Cluster is deliberately thin — a join point naming a pod/service CIDR and pointing at two provider objects. The KubeadmControlPlane holds the interesting detail: how many members (always odd, because etcd needs a quorum), which version, and the kubeadm configuration stamped into each. The examples below use the long-established v1beta1 version of the CAPI API groups; newer Cluster API releases also serve a v1beta2 version of the same kinds with some fields reshaped, and infrastructure providers version their own group independently — so check kubectl api-resources and kubectl explain on your management cluster before copying any of this verbatim.

apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: prod-eu-west
  namespace: fleet
  labels:
    env: prod                                  # used later by ClusterResourceSet / ApplicationSet
spec:
  clusterNetwork:
    pods:     { cidrBlocks: ["192.168.0.0/16"] }
    services: { cidrBlocks: ["10.128.0.0/12"] }
  controlPlaneRef:                             # who owns api-server + etcd
    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
    kind: KubeadmControlPlane
    name: prod-eu-west-cp
  infrastructureRef:                           # who owns VPC, subnets, the CP load balancer
    apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
    kind: AWSCluster
    name: prod-eu-west
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
  name: prod-eu-west-cp
  namespace: fleet
spec:
  replicas: 3                                  # ODD — stacked etcd quorum. 1 for dev, 3 or 5 for prod
  version: v1.30.2                             # bump this → rolling control-plane replacement
  machineTemplate:
    infrastructureRef:
      apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
      kind: AWSMachineTemplate
      name: prod-eu-west-cp                    # templates are IMMUTABLE — create a new one to change
  rolloutStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                              # add one new member before removing an old one
  kubeadmConfigSpec:
    clusterConfiguration:
      apiServer:
        extraArgs:                             # api-server flags, minus the leading "--"
          audit-log-path: /var/log/audit.log
          # NOTE: audit logging also needs a policy file on the node — ship it with
          # kubeadmConfigSpec.files and mount it via apiServer.extraVolumes.
    initConfiguration:
      nodeRegistration:
        kubeletExtraArgs: { cloud-provider: external }
    joinConfiguration:
      nodeRegistration:
        kubeletExtraArgs: { cloud-provider: external }

A worker pool, and a health check that repairs it

The MachineDeployment should feel like a Deployment; note that version lives on the machine template spec, not the top level. Beside it, a MachineHealthCheck turns “that node has been NotReady for ten minutes” into an automatic replacement — what makes a fleet self-repairing rather than merely self-describing.

apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: prod-eu-west-md-0
  namespace: fleet
spec:
  clusterName: prod-eu-west
  replicas: 6                                  # scale the pool like any Deployment
  selector:
    matchLabels: {}                            # left empty on purpose: the webhook defaults both the
                                               # selector and the template labels to
                                               # cluster.x-k8s.io/deployment-name: prod-eu-west-md-0
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  template:
    spec:
      clusterName: prod-eu-west
      version: v1.30.2                         # NOTE: on template.spec, not on spec
      bootstrap:
        configRef:                             # bootstrap provider: VM → Kubernetes node
          apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
          kind: KubeadmConfigTemplate
          name: prod-eu-west-md-0
      infrastructureRef:                       # infrastructure provider: the actual VM
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
        kind: AWSMachineTemplate
        name: prod-eu-west-md-0
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineHealthCheck
metadata:
  name: prod-eu-west-workers-unhealthy
  namespace: fleet
spec:
  clusterName: prod-eu-west
  nodeStartupTimeout: 15m                      # grace period before a new node is judged
  maxUnhealthy: 40%                            # CIRCUIT BREAKER: above this, stop remediating
  selector:
    matchLabels:
      cluster.x-k8s.io/deployment-name: prod-eu-west-md-0
  unhealthyConditions:
    - type: Ready
      status: "False"
      timeout: 10m                             # NotReady for 10m → delete & replace the Machine
    - type: Ready
      status: Unknown
      timeout: 10m

A ClusterClass, and the tiny Cluster that uses it

Past two or three clusters, stop writing the objects above per cluster. Author one ClusterClass and let each cluster be a short topology — the moment CAPI becomes a genuine self-service API rather than a provisioning tool.

apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
  name: standard-aws
  namespace: fleet
spec:
  controlPlane:
    ref:
      apiVersion: controlplane.cluster.x-k8s.io/v1beta1
      kind: KubeadmControlPlaneTemplate
      name: standard-aws-cp
    machineInfrastructure:
      ref:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
        kind: AWSMachineTemplate
        name: standard-aws-cp
  infrastructure:
    ref:
      apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
      kind: AWSClusterTemplate
      name: standard-aws
  workers:
    machineDeployments:
      - class: general-purpose                 # a named worker CLASS consumers pick by name
        template:
          bootstrap:
            ref:
              apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
              kind: KubeadmConfigTemplate
              name: standard-aws-worker
          infrastructure:
            ref:
              apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
              kind: AWSMachineTemplate
              name: standard-aws-worker
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: team-blue
  namespace: fleet
spec:
  topology:                                    # the whole cluster, in a dozen lines
    class: standard-aws
    version: v1.30.2                           # ONE field upgrades control plane AND workers
    controlPlane:
      replicas: 3
    workers:
      machineDeployments:
        - class: general-purpose
          name: md-0
          replicas: 4
🦆 Dot’s-eye view

“I needed an isolated cluster for a load test. No ticket, and I never learned what a MachineSet is — I copied twelve lines into the fleet repo, changed the name and the replica count, and opened a PR. Twenty minutes after it merged a kubeconfig appeared in the vault path my team already uses. When the test finished I deleted the folder and the cluster went away. First time infrastructure ever felt like code to me.”

Day-to-day commands

☺ Like you’re 10: Most days you use plain kubectl. clusterctl is for setting up the workshop and moving it.

The mental model: clusterctl manages the management cluster (installing and upgrading providers, generating and moving manifests), while kubectl manages the fleet (creating, scaling and upgrading clusters, because they are just objects). Reaching for clusterctl to change a cluster usually means you are holding it wrong.

Bootstrapping a management cluster

# 0. Somewhere to run the controllers — a throwaway kind cluster is the classic bootstrap.
kind create cluster --name capi-bootstrap
# For the DOCKER provider only, that kind cluster must mount the host Docker socket, so create
# it from a kind config with an extraMounts entry for /var/run/docker.sock instead of the line above.

# 1. Enable the ClusterClass feature and initialise core + provider controllers.
export CLUSTER_TOPOLOGY=true
clusterctl init --infrastructure aws            # core + kubeadm bootstrap + KCP + CAPA
clusterctl init --infrastructure docker         # CAPD: local clusters-in-containers, great for learning
clusterctl init --infrastructure azure,vsphere  # several infra providers on one management cluster

# 2. What did that install, and is it healthy?
kubectl get providers -A                        # core / bootstrap / control-plane / infrastructure + versions
kubectl get pods -n capi-system                 # the core controller
kubectl get pods -n capa-system                 # the AWS provider controller (one namespace per provider)

Creating, inspecting and reaching a workload cluster

# Generate manifests from a provider's template — then READ them before applying.
clusterctl generate cluster prod-eu-west \
  --kubernetes-version v1.30.2 \
  --control-plane-machine-count=3 \
  --worker-machine-count=6 \
  --target-namespace fleet > clusters/prod-eu-west.yaml

# In a GitOps platform you commit that file; Argo CD or Flux does the apply.
kubectl apply -f clusters/prod-eu-west.yaml

# Watch it come up. `describe` renders the whole object tree with conditions — the best CAPI command.
clusterctl describe cluster prod-eu-west -n fleet
kubectl get cluster,kubeadmcontrolplane,machinedeployment,machine -n fleet
kubectl get machines -n fleet -o wide           # phase: Provisioning → Provisioned → Running

# The kubeconfig is a Secret named <cluster>-kubeconfig; clusterctl extracts it for you.
clusterctl get kubeconfig prod-eu-west -n fleet > /tmp/prod-eu-west.kubeconfig
kubectl --kubeconfig /tmp/prod-eu-west.kubeconfig get nodes
# Nodes NotReady? Expected — CAPI installs no CNI. Install one (e.g. Cilium/Calico) or use a ClusterResourceSet.

Scaling, upgrading, pivoting and deleting

# Scale a worker pool — ordinary Kubernetes, no special tooling.
kubectl scale machinedeployment prod-eu-west-md-0 -n fleet --replicas=10

# Upgrade: change the version. Control plane FIRST, then workers, never more than one minor at a time.
kubectl patch kubeadmcontrolplane prod-eu-west-cp -n fleet --type=merge \
  -p '{"spec":{"version":"v1.31.0"}}'
kubectl patch machinedeployment prod-eu-west-md-0 -n fleet --type=merge \
  -p '{"spec":{"template":{"spec":{"version":"v1.31.0"}}}}'
# (With a ClusterClass topology, both of the above collapse to one edit of spec.topology.version.)

# Pause / resume a rollout, or roll a pool for a new image without a version change.
clusterctl alpha rollout pause   machinedeployment/prod-eu-west-md-0 -n fleet
clusterctl alpha rollout resume  machinedeployment/prod-eu-west-md-0 -n fleet
clusterctl alpha rollout restart machinedeployment/prod-eu-west-md-0 -n fleet
clusterctl alpha rollout undo    machinedeployment/prod-eu-west-md-0 -n fleet

# Upgrade the CAPI providers themselves (NOT the clusters). Always plan first.
clusterctl upgrade plan
# Use the contract version that `upgrade plan` reports for the target release.
clusterctl upgrade apply --contract v1beta1

# PIVOT: move CAPI objects from the bootstrap cluster onto the real management cluster.
clusterctl move --to-kubeconfig=/tmp/mgmt.kubeconfig --namespace fleet

# Delete a cluster — ALWAYS via the Cluster object, so finalizers unwind the cloud resources in order.
kubectl delete cluster prod-eu-west -n fleet

Everything above also lives in the command reference if you want it on one page.

Gotchas and failure modes

☺ Like you’re 10: Here are the ways the robot workshop goes wrong — mostly by being too keen to fix things.

Provider version skew and the contract

Core and providers talk across a versioned contract (for example v1beta1). Upgrade core without upgrading a provider that implements only an older contract and that provider quietly stops reconciling — clusters do not break, they stop responding to changes, which is worse because it is silent. Always run clusterctl upgrade plan, which prints the current and target contract per provider, then clusterctl upgrade apply. Never kubectl apply a provider’s release manifest by hand on a management cluster clusterctl manages — you will desynchronise its provider inventory.

clusterctl move — the pivot that eats your fleet

Moving CAPI objects between management clusters is the most dangerous routine operation. The target must already have the same providers at compatible versions installed, or the objects land with nothing to reconcile them. The move pauses reconciliation on the source and re-creates objects on the target; delete the source namespace too eagerly, or run move mid-provisioning, and you orphan cloud infrastructure no controller now owns. Safe sequence: quiesce changes, move with an explicit --namespace, verify on the target with clusterctl describe cluster, and only then tear down the source.

⚠ MachineHealthCheck remediation loops

A MachineHealthCheck deletes unhealthy Machines so they get replaced — wonderful when one node has a bad disk, catastrophic when the cause is cluster-wide: a broken CNI, an expired certificate, a bad AMI. Every replacement also fails to become Ready, MHC deletes it too, and you have a churn loop that burns cloud spend and can shred the cluster. Use all three guards: maxUnhealthy (or unhealthyRange) so remediation stops once too many machines are sick — a circuit breaker, not a nicety; a nodeStartupTimeout generous enough that slow-booting nodes are not killed before they join; and the escape hatch — annotate a Machine with cluster.x-k8s.io/skip-remediation to hold it still while you debug. Land in this loop mid-incident and pausing the MHC comes before anything else; reliability & incidents has the wider pattern.

Immutable templates and the fields that surprise people

Infrastructure templates such as AWSMachineTemplate are immutable: you cannot edit the instance type or AMI in place. Create a new template with a new name and point the MachineDeployment or KubeadmControlPlane at it, triggering a rolling replacement — the same discipline as immutable container images. Two traps in the same family: version lives under spec.template.spec.version, and setting it at the top level does nothing; and an even replicas count on a KubeadmControlPlane is rejected by its validating webhook when etcd is stacked, because an even number buys no quorum advantage and a worse failure profile than an odd one — 1, 3 or 5, never 2 or 4.

The quieter operational traps

No CNI — a fresh cluster’s nodes stay NotReady until you install one; automate it with a ClusterResourceSet or CAAPH (see networking and Cilium). Skipping minors — kubeadm does one minor at a time, so 1.29 → 1.31 is two hops. Deleting the wrong object — always kubectl delete cluster; deleting Machines or infra objects directly orphans cloud resources. Quota — a maxSurge: 1 upgrade needs N+1 machines, and exhaustion appears as Machines stuck Provisioning with the real reason in the infrastructure object’s conditions, not the Cluster’s. PodDisruptionBudgets — CAPI drains before deleting, so an unsatisfiable PDB stalls an upgrade quietly and indefinitely (workload triage). When a cluster will not come up the ladder never changes: clusterctl describe cluster, then conditions on the failing object, then that provider’s logs — the troubleshooting playbook generalises it.

1 · edit Git version: v1.31.0 2 · surge new Machine created 3 · wait Ready node joins & passes 4 · drain old cordon · respect PDBs 5 · delete old VM destroyed repeat, one machine at a time, until the whole pool is at the new version nothing is upgraded in place — a node is replaced, never patched
🦫 Benny’s workshop · 30 min

No cloud account needed — the Docker provider (CAPD) runs whole clusters as containers. With Docker running, create a kind cluster from a config file that adds an extraMounts entry for /var/run/docker.sock (CAPD drives Docker through that socket, so a plain kind create cluster will not do), then export CLUSTER_TOPOLOGY=true && clusterctl init --infrastructure docker. Create one with clusterctl generate cluster demo --flavor development --kubernetes-version v1.30.2 --worker-machine-count=2 | kubectl apply -f - and watch clusterctl describe cluster demo paint the tree. Pull the kubeconfig with clusterctl get kubeconfig demo; the nodes are NotReady — install a CNI and watch them flip. Then the payoff: kubectl delete machine on one worker and watch a replacement appear, and patch template.spec.version to see the rolling replacement live.

Alternatives and when to choose it

☺ Like you’re 10: There are other ways to get clusters. Here’s when each one is the sensible answer.

CAPI is powerful and not free: you now operate a management cluster and a set of controllers. Be honest about when a simpler tool wins.

The comparison that decides it

OptionModelBest whenCosts you
Cluster APIKubernetes controllers reconciling Cluster/Machine objects, continuouslyA fleet of clusters; hybrid or on-prem; you want one uniform, GitOps-able API across clouds and bare metal; declarative, rollback-able upgradesYou must run and upgrade a management cluster and its providers; provider maturity varies; a genuine learning curve
Terraform / OpenTofu (see IaC)Run-to-completion CLI with a state fileA handful of clusters; you already run Terraform for accounts and networking; you need breadth across non-Kubernetes resourcesNo continuous reconciliation — drift waits for the next plan; upgrades are human-triggered runs; state file operations
Managed cluster services (EKS/AKS/GKE, via console or CLI)Cloud provider owns the control planeOne cloud, a small number of clusters, no on-prem, and you want the least machinery possibleSnowflakes and click-ops unless wrapped in IaC; cloud-specific; no uniform API across environments
CAPI with a managed control-plane provider (e.g. AWSManagedControlPlane)CAPI objects front an EKS/AKS control planeYou want the CAPI API and GitOps workflow but would rather the cloud ran etcdFewer knobs than self-managed; feature parity lags the cloud’s own API
CrossplaneControl plane for cloud resources, including some cluster typesYou are already standardising infrastructure self-service on Crossplane XRDs and want clusters in the same catalogueCluster lifecycle depth (rolling node upgrades, health remediation) is CAPI’s specialty, not Crossplane’s — many teams compose the two
kubeadm / kops by handImperative CLI per clusterLearning, or one long-lived cluster you truly hand-tendPets. No reconciliation, no fleet story, upgrades are events

A practical rule

Count your clusters and your clouds. One or two on one cloud: use the managed service wrapped in Terraform and spend your energy elsewhere. Ten or more, or any mix of cloud and on-prem, or a hard requirement that clusters be reproducible from Git: adopt Cluster API — and adopt ClusterClass from day one so you never accumulate per-cluster YAML. In between, the deciding question is upgrades: if patching Kubernetes across the estate already costs a week per cycle, CAPI pays for itself immediately. See The Tool Landscape for how this sits among the platform’s other components.

🎬 At the Platform Guild
🦊

Foxy: Upgrade day. I SSH’d into the nodes and ran the package manager. Only took four hours!

🦫

Benny: Four hours per cluster, Foxy. We have thirty-one. With CAPI I change spec.topology.version in a PR and the controller surges a new node, waits for Ready, drains the old one, deletes it — then does the next.

🦉

Professor Owl: And note why that’s cheap: nothing is patched. A machine is replaced. No node can drift from its template, because no node lives long enough to.

👺

Gizmo: Love it. So I’ll set a MachineHealthCheck to nuke anything NotReady for sixty seconds. Maximum self-healing! 🤑

🐢

Timmy: …and the first time a CNI upgrade goes wrong, every replacement is also NotReady, and you’ve built a machine shredder. Set maxUnhealthy. It’s a circuit breaker, not a formality.

🦆

Dot: All I know is I asked for a load-test cluster in a pull request and got a kubeconfig. Nobody made me learn what a MachineSet is. Please keep it that way.

Exam relevance and going further

☺ Like you’re 10: You won’t be asked to run this on exam day — but you will be asked what it’s for. And you can’t look up its website.

Cluster API is not on the official CNPE tool list, so no task will hand you a management cluster and ask for a MachineDeployment. What is examinable is the thinking it represents: declarative infrastructure, continuous reconciliation versus one-shot apply, immutable infrastructure, clusters as cattle, the hub-and-spoke fleet shape — which surface in architecture-flavoured questions and anywhere the blueprint touches IaC.

The documentation allowlist — read this twice

⚠ The Cluster API Book is not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. Cluster API is a Kubernetes sub-project, but its documentation lives at cluster-api.sigs.k8s.io — a different domain from kubernetes.io — so it is not on the allowlist. Nothing about CAPI can be looked up. Anything you need must be in your head, and the manifests genuinely worth memorising are drilled on Know Cold.

⚖ CNPA vs CNPE — That allowlist is a CNPE mechanic — CNPE is hands-on, so it grants a narrow set of live lookups. CNPA is stricter still: it's a fully closed-book multiple-choice exam with zero external resources and zero lookups of any kind, so nothing here would be reachable there either. Still, the CAPI concepts above — management vs. workload clusters, immutable infrastructure, the provider roles — are exactly the kind of concept-level knowledge CNPA's closed-book recall tests.

What to be able to do without notes

Explain what CAPI is and why a platform would use it. Distinguish management from workload clusters and say why the hub must not be a runtime dependency of the spokes. Name the three provider roles with an example of each. Say what changing version does to the nodes — surge, wait, cordon, drain, delete, repeat — and why that is immutable infrastructure. Recite MachineDeploymentMachineSetMachine and its pod-side analogue. Explain MachineHealthCheck and why maxUnhealthy exists. Know a fresh cluster has no CNI. And say where CAPI ends and GitOps begins.

Official resources for after the exam

Outside the exam the canonical source is the Cluster API Book — the Quick Start, ClusterClass and Troubleshooting chapters repay careful reading — plus github.com/kubernetes-sigs/cluster-api, cluster-api-aws.sigs.k8s.io and capz.sigs.k8s.io. For the concepts underneath, kubernetes.io on controllers is on the allowlist and is the best free revision. On this site, pair this page with Multi-Cluster & Fleet, IaC & Control Planes, Kubebuilder, scaling & scheduling, and the glossary.

🐢 Timmy’s checkpoint

1. What is the difference between a management cluster and a workload cluster, and why must the former not be a runtime dependency of the latter? 2. Name the three provider roles and one example of each. 3. Walk through exactly what CAPI does when you change version on a MachineDeployment. 4. Where does version live on a MachineDeployment, and what happens if you put it at the top level? 5. Your new cluster’s nodes are all NotReady and nothing is wrong with the machines. What did you forget? 6. What is a remediation loop, and which three settings guard against one? 7. What does clusterctl move do, and name two preconditions before running it. 8. During the exam, where can you look up the Cluster schema?

Check your answers
  1. The management cluster runs the CAPI controllers and holds the Cluster/Machine objects describing the fleet; a workload cluster is one it provisions, and runs your apps. The hub must not be a runtime dependency so that if it is down, every workload cluster keeps serving — you just cannot make fleet-level changes until it is back.
  2. Infrastructure (CAPA/AWS, CAPZ/Azure, CAPG/GCP, vSphere, Metal3, CAPD/Docker) creates VMs and networks; bootstrap (Kubeadm/CABPK, also Talos or RKE2) turns a VM into a Kubernetes node via cloud-init; control plane (KubeadmControlPlane, or managed variants like AWSManagedControlPlane) owns the api-server/etcd tier and its rollout.
  3. A rolling, immutable replacement: it provisions a new Machine at the new version (surging by maxSurge), waits for the Node to become Ready, cordons and drains the old node respecting PodDisruptionBudgets, deletes the old Machine and its VM, then repeats for the next one. Nothing is upgraded in place.
  4. Under spec.template.spec.version. Setting it at the top level of spec does nothing — a common and very quiet mistake.
  5. The CNI. CAPI creates an empty cluster and deliberately installs no network plugin; nodes stay NotReady until one is applied — automate it with a ClusterResourceSet, CAAPH, or your GitOps controller.
  6. A MachineHealthCheck deleting machines that fail for a cluster-wide reason, so every replacement also fails and the fleet churns forever. Guards: maxUnhealthy (or unhealthyRange) as a circuit breaker, a generous nodeStartupTimeout, and the cluster.x-k8s.io/skip-remediation annotation as a manual hold.
  7. It moves CAPI objects from one management cluster to another — the “pivot” that makes a new management cluster self-managing. Preconditions: the target must already have the same providers installed at compatible contract versions, and nothing should be mid-provisioning; back up first and verify with clusterctl describe cluster on the target before dismantling the source.
  8. You can’t. cluster-api.sigs.k8s.io is not kubernetes.io, so it is off the allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). CAPI is not on the CNPE tool list anyway — know the concepts, and drill any memorised manifests on Know Cold.