Kubernetes Multi-Cluster Management with Cluster API
Kubernetes multi-cluster Cluster API (CAPI) provides a declarative, Kubernetes-native way to provision and manage clusters across any infrastructure. Instead of stitching together cloud-specific CLIs and bespoke Terraform scripts, you define clusters as Kubernetes resources and let CAPI handle provisioning, scaling, and lifecycle. In other words, it brings the same reconciliation model to cluster infrastructure that Kubernetes already brings to application workloads. As a result, a fleet of clusters becomes something you describe in YAML and review in pull requests rather than something you click through in a console.
This guide covers setting up a management cluster, provisioning workload clusters across AWS, Azure, and bare metal, driving the cluster lifecycle with GitOps, and handling day-2 operations such as upgrades and scaling. Whether you run 5 clusters or 500, the approach stays consistent — which is precisely the point. Importantly, that consistency comes at the cost of operational complexity, so we will be equally clear about when this tooling is overkill.
Cluster API Architecture
CAPI uses a dedicated management cluster to manage workload clusters. The management cluster runs the CAPI controllers, which watch for resources like Cluster, Machine, and MachineDeployment and reconcile them against real infrastructure. Each cloud has its own infrastructure provider that translates generic CAPI objects into provider-specific API calls — so the same Cluster abstraction maps onto an AWS VPC and EC2 fleet, an Azure VNet and VM scale set, or vSphere VMs, depending on which provider you install.
Cluster API Architecture
Management Cluster (runs CAPI controllers):
├── CAPI Core Controller
├── Bootstrap Provider (kubeadm)
├── Control Plane Provider (kubeadm)
├── Infrastructure Provider (AWS/Azure/vSphere)
└── Watches: Cluster, Machine, MachineDeployment CRDs
Workload Cluster A (AWS):
├── 3 control plane nodes (m6i.xlarge)
├── MachineDeployment: 5 worker nodes
└── Auto-managed by CAPI controllers
Workload Cluster B (Azure):
├── 3 control plane nodes (Standard_D4s_v3)
├── MachineDeployment: 10 worker nodes
└── Auto-managed by CAPI controllers
The separation of concerns is the architecture’s defining strength. The bootstrap provider (typically kubeadm) generates the cloud-init data that turns a bare VM into a Kubernetes node; the control plane provider owns the etcd and API-server lifecycle; and the infrastructure provider owns the underlying compute and networking. Because these are independent controllers reconciling independent custom resources, you can, for example, swap kubeadm for a different bootstrap mechanism without touching how AWS networking is provisioned.
Setting Up the Management Cluster
# Initialize a management cluster
# Start with a kind cluster or existing cluster
kind create cluster --name capi-management
# Install clusterctl CLI
curl -L https://github.com/kubernetes-sigs/cluster-api/releases/latest/download/clusterctl-linux-amd64 -o clusterctl
chmod +x clusterctl && sudo mv clusterctl /usr/local/bin/
# Initialize with AWS infrastructure provider
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=<your-access-key>
export AWS_SECRET_ACCESS_KEY=<your-secret-key>
clusterctl init \
--infrastructure aws \
--bootstrap kubeadm \
--control-plane kubeadm
# Verify controllers are running
kubectl get pods -n capi-system
kubectl get pods -n capa-system
One pattern that production teams adopt early is the pivot. You bootstrap CAPI on a throwaway kind cluster, use it to create a small, durable management cluster on real infrastructure, and then run clusterctl move to transfer all CAPI resources to that permanent home. Doing so removes the laptop-bound kind cluster from the critical path. Furthermore, the management cluster should be hardened like any production control plane — backed-up etcd, restricted RBAC, and tightly scoped cloud credentials — because it holds the keys to every workload cluster it manages.
Provisioning a Workload Cluster
# cluster-production.yaml — Declarative cluster definition
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: production-us-east
namespace: clusters
labels:
environment: production
region: us-east-1
spec:
clusterNetwork:
pods:
cidrBlocks: ["192.168.0.0/16"]
services:
cidrBlocks: ["10.96.0.0/12"]
serviceDomain: cluster.local
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
name: production-us-east-cp
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSCluster
name: production-us-east
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSCluster
metadata:
name: production-us-east
namespace: clusters
spec:
region: us-east-1
sshKeyName: capi-key
network:
vpc:
cidrBlock: "10.0.0.0/16"
subnets:
- availabilityZone: us-east-1a
cidrBlock: "10.0.1.0/24"
isPublic: false
- availabilityZone: us-east-1b
cidrBlock: "10.0.2.0/24"
isPublic: false
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
name: production-us-east-cp
namespace: clusters
spec:
replicas: 3
version: v1.31.0
machineTemplate:
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSMachineTemplate
name: production-cp-template
kubeadmConfigSpec:
initConfiguration:
nodeRegistration:
kubeletExtraArgs:
cloud-provider: external
clusterConfiguration:
apiServer:
extraArgs:
audit-log-maxage: "30"
audit-log-maxbackup: "10"
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: production-us-east-workers
namespace: clusters
spec:
clusterName: production-us-east
replicas: 5
selector:
matchLabels: {}
template:
spec:
clusterName: production-us-east
version: v1.31.0
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
name: production-worker-config
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSMachineTemplate
name: production-worker-template
Notice that the manifest is portable in structure but not in detail. The Cluster and MachineDeployment objects are cloud-agnostic, while the AWSCluster and AWSMachineTemplate are provider-specific. Consequently, templating becomes essential the moment you manage more than a couple of clusters. The clusterctl generate cluster command and ClusterClass — a topology abstraction that lets many clusters inherit from one blueprint — both exist precisely to keep dozens of near-identical manifests from drifting apart. One important caveat: there is no network plugin in the spec above. CAPI provisions the nodes, but you must still install a CNI (Calico, Cilium, or similar) before the cluster reports Ready, which trips up newcomers who expect a turnkey result.
GitOps-Driven Cluster Lifecycle
Moreover, integrating CAPI with a GitOps controller such as Flux or Argo CD turns cluster provisioning and upgrades into reviewable, auditable Git operations. You store the cluster manifests in a repository, and the controller continuously reconciles the cluster state to match the declared intent. As a result, “create a new region” becomes a pull request, and a bad change can be rolled back the same way you roll back application code.
# flux-kustomization.yaml — GitOps for cluster management
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: clusters
namespace: flux-system
spec:
interval: 5m
path: ./clusters/production
prune: false # Never auto-delete clusters!
sourceRef:
kind: GitRepository
name: infrastructure
healthChecks:
- apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
name: production-us-east
namespace: clusters
timeout: 30m # Cluster provisioning takes time
Two settings in that manifest are load-bearing. First, prune: false is a deliberate safety measure — with pruning enabled, deleting a file in Git would tear down a live cluster, which is rarely what you want for stateful, expensive infrastructure. Second, the generous timeout reflects reality: provisioning a control plane, waiting on cloud APIs, and bootstrapping nodes routinely takes many minutes, so a short timeout produces false “failed” signals. A common refinement is to gate cluster deletion behind a separate, manual workflow so that no single merge can ever destroy production capacity.
Day-2 Operations: Upgrades and Scaling
# Rolling Kubernetes upgrade — just change the version
kubectl patch kubeadmcontrolplane production-us-east-cp \
--namespace clusters \
--type merge \
--patch '{"spec":{"version":"v1.32.0"}}'
# CAPI performs rolling upgrade:
# 1. Creates new control plane node with v1.32
# 2. Waits for it to be healthy
# 3. Removes old control plane node
# 4. Repeats for all control plane nodes
# 5. Upgrades workers via MachineDeployment rollout
# Scale workers
kubectl scale machinedeployment production-us-east-workers \
--namespace clusters --replicas=10
# Monitor cluster status
kubectl get clusters -n clusters
kubectl get machines -n clusters
clusterctl describe cluster production-us-east -n clusters
Day-2 is where CAPI’s declarative model pays the largest dividend. Because upgrades happen by editing the desired version and letting controllers replace machines one at a time, you get immutable, rolling upgrades rather than risky in-place kubeadm upgrade runs on live nodes. To make this safer still, attach a MachineHealthCheck so that nodes which fail to come up healthy are automatically remediated, and always upgrade a single minor version at a time — Kubernetes does not support skipping minor versions, and CAPI will faithfully refuse to do so. For predictable cost and capacity, the cluster-autoscaler integrates with MachineDeployment objects, letting worker count track real demand instead of a static replica number.
When NOT to Use Cluster API
Cluster API adds real operational weight, and it is honest to say so. If you run fewer than a handful of clusters, managed services such as EKS, AKS, or GKE driven by Terraform are simpler and demand far less in-house expertise. Additionally, the management cluster is a meaningful single point of failure: while existing workload clusters keep running if it goes down, you cannot provision, scale, or upgrade them until it recovers — which is why backups and high availability for that cluster are not optional.
Furthermore, CAPI is a poor fit for edge or IoT scenarios where clusters sit behind NATs or on unreliable links, because the management cluster needs reliable network reach to reconcile them. The trade-off, then, is clear: CAPI rewards organizations that manage many clusters, need uniform provisioning across clouds, and staff a platform team capable of operating the management plane. As a result, smaller teams are usually better served starting with managed Kubernetes and reaching for CAPI only once multi-cluster sprawl becomes a genuine management burden rather than a hypothetical one.
Key Takeaways
- Pivot CAPI off your bootstrap kind cluster onto a hardened, backed-up management cluster
- Use ClusterClass or templating so many clusters inherit from one reviewed blueprint
- Remember to install a CNI — CAPI provisions nodes but does not pick your network plugin
- Keep
prune: falseand gate deletions so no single merge can destroy a cluster - Attach MachineHealthChecks and upgrade one minor Kubernetes version at a time
For related DevOps topics, explore our guide on GitOps with Flux and ArgoCD and Terraform infrastructure automation. The Cluster API book and CAPI GitHub repository provide detailed reference documentation.
In conclusion, Kubernetes multi-cluster Cluster API brings declarative, version-controlled management to cluster infrastructure: you describe clusters as YAML, store them in Git, and let controllers reconcile provisioning and lifecycle. Combined with GitOps, the result is auditable, reproducible cluster management across any cloud. Start with a dedicated management cluster, prove the pattern on a single workload cluster, and expand only as your platform team — and your need for consistency — matures.