MachinePool vs MachineDeployment: A Platform Engineer's Guide to CAPI Worker Nodes

Learn how to use MachinePool and MachineDeployment to manage worker nodes in a Kubernetes cluster

  • Satyam Bhardwaj
  • 7 min read

Introduction

If you’re not familiar with Cluster API, let me take you back to first principles. Kubernetes is a container orchestration engine that was built for running applications at scale. But as adoption grew, organizations found themselves managing not one cluster, but hundreds of Kubernetes clusters across environments: from hyperscalers to on-prem, from dev sandboxes to production fleets. The native solution to this problem emerged from the Kubernetes SIGs community: Cluster API (CAPI).

When defining worker node topology, one of the first decisions is which CAPI primitive to use: MachineDeployment or MachinePool. Both produce working Kubernetes nodes, but they differ in who owns the machine lifecycle. This post breaks down that difference, walks through the operational trade-offs, and includes benchmark numbers from a real Azure scale-up test.

The Two Models Explained

MachineDeployment: CAPI-Orchestrated Scaling

MachineDeployment follows a familiar pattern and if you’ve used Kubernetes Deployments, you’ll feel at home. CAPI creates and manages every object in the hierarchy:

MachineDeployment
       │
       ├──► MachineSet (manages replicas)
       │         │
       │         ├──► Machine-1 ──► InfraMachine-1 ──► VM-1
       │         ├──► Machine-2 ──► InfraMachine-2 ──► VM-2
       │         └──► Machine-N ──► InfraMachine-N ──► VM-N
       │
       └──► KubeadmConfigTemplate
                  │
                  ├──► KubeadmConfig-1 ──► Bootstrap Secret-1
                  ├──► KubeadmConfig-2 ──► Bootstrap Secret-2
                  └──► KubeadmConfig-N ──► Bootstrap Secret-N

CAPI creates a Machine object for every node, each with its own InfraMachine, KubeadmConfig, and bootstrap secret. Every machine’s state is tracked in the management cluster, giving you full lifecycle visibility.

MachinePool: Cloud-Native Delegation

MachinePool delegates scaling to your cloud provider’s native scaling group:

MachinePool
       │
       ├──► InfraMachinePool (wraps cloud scaling group)
       │         │
       │         └──► Cloud Scaling Group (VMSS / ASG / MIG)
       │                    │
       │                    ├──► VM-1
       │                    ├──► VM-2
       │                    └──► VM-N
       │
       └──► KubeadmConfig (shared by all instances)
                  │
                  └──► Single Bootstrap Secret

Regardless of replica count, CAPI creates one InfraMachinePool and one shared bootstrap configuration. The cloud provider manages the actual VMs. This means fewer CAPI objects and the cloud provider handles fleet-level concerns like AZ distribution, instance replacement, and scaling orchestration natively.

Scope note: Both primitives manage worker nodes. Control plane nodes are managed separately by KubeadmControlPlane, which requires unique certificates, etcd membership, and differentiated init/join behavior - concerns that don’t apply to interchangeable worker compute.

How They Actually Differ

Object Creation Flow

MachineDeployment (CAPI drives, cloud follows):

  1. User sets spec.replicas: 50
  2. MachineDeployment controller creates/updates a MachineSet
  3. MachineSet controller creates 50 Machine objects
  4. Machine controller creates 50 InfraMachine objects
  5. Provider controller provisions 50 VMs

MachinePool (cloud drives, CAPI observes):

  1. User sets spec.replicas: 50
  2. MachinePool controller updates InfraMachinePool desired count
  3. Provider controller tells the cloud scaling group to provision VMs
  4. Cloud provider provisions VMs using its native orchestration
  5. InfraMachinePool reports back status.replicas: 50
  6. MachinePool controller observes and updates CAPI status

Rolling Update Behavior

MachineDeployment: CAPI creates a new MachineSet with the updated spec, scales it up, cordons and drains each old node, then scales down the old MachineSet.

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

MachinePool: CAPI updates the InfraMachinePool spec. The cloud provider rolls out the change using its native mechanism: Azure uses VMSS Rolling Upgrade Policy, AWS uses Instance Refresh, GCP uses MIG update. These are the same battle-tested update mechanisms that cloud-native teams use outside of CAPI. CAPZ, for example, progressively cordons, drains, and replaces individual machines in the scale set during rolling upgrades. Check your provider’s docs for specifics on cordon/drain behavior.

Health Checking and Failure Recovery

MachineDeployment + MachineHealthCheck: MHC watches Machine objects and their corresponding Nodes. When a node is unhealthy, MHC deletes the Machine. The MachineSet creates a replacement.

MachinePool: Health checking can work at two levels. If the provider supports the MachinePool Machines feature (by setting status.infrastructureMachineKind), MachineHealthCheck works with individual pool instances, giving you CAPI-level observability alongside cloud-native healing. Without it, the scaling group still detects and replaces failed instances on its own using its native health probes, which is the same recovery model cloud-native teams rely on outside of CAPI.

MachinePool Infrastructure Provider Support

MachinePool requires the provider to implement a scaling group abstraction. The major hyperscalers support it; environments without native scaling groups don’t:

ProviderMachinePool SupportDetails
Azure (CAPZ)YesAzureMachinePool backed by VMSS. Also AzureManagedMachinePool for AKS.
AWS (CAPA)YesAWSMachinePool backed by ASG. Also AWSManagedMachinePool for EKS.
GCP (CAPG)PartialGCPManagedMachinePool for GKE exists. Self-managed MachinePool via MIG is tracked but not actively developed.
vSphere (CAPV)NoNo native scaling group primitive.
Bare metalNoNo scaling group concept on physical hardware.
Docker (CAPD)YesDockerMachinePool for development/testing.

Benchmark: Scale-Up on Azure

To ground the architectural differences in real numbers, I ran a scale-up benchmark on Azure using CAPZ. Three configurations were tested side by side: a MachineDeployment, a MachinePool (VMSS-backed), and an AKS managed node pool. Each scaled from 5 to 20 worker nodes on identical Standard_D2s_v3 instances.

Test Setup

Both clusters share the same configuration:

  • Management cluster: CAPI v1.10+ with CAPZ, running on a single Standard_D2s_v3 control plane node
  • Worker node type: Standard_D2s_v3, Kubernetes v1.32.0
  • Starting replicas: 5
  • Target replicas: 20
  • Measurement: Wall-clock time from spec.replicas patch to all 15 new nodes reporting Ready

Here are the key resources used for each cluster. Both use the same VM size, OS disk configuration, and Kubernetes version - the only difference is the CAPI primitive and the infrastructure it maps to.

MachineDeployment - each replica becomes an individual Azure VM managed by a MachineSet:

apiVersion: cluster.x-k8s.io/v1beta2
kind: MachineDeployment
metadata:
  name: md-bench-azure-workers
spec:
  clusterName: md-bench-azure
  replicas: 5
  template:
    spec:
      version: v1.32.0
      bootstrap:
        configRef:
          kind: KubeadmConfigTemplate
          name: md-bench-azure-workers
      infrastructureRef:
        kind: AzureMachineTemplate
        name: md-bench-azure-workers
  rollout:
    strategy:
      type: RollingUpdate
      rollingUpdate:
        maxSurge: 1
        maxUnavailable: 0

MachinePool - replicas are delegated to an Azure VMSS:

apiVersion: cluster.x-k8s.io/v1beta2
kind: MachinePool
metadata:
  name: mp-bench-azure-workers
spec:
  clusterName: mp-bench-azure
  replicas: 5
  failureDomains:
    - "1"
    - "2"
    - "3"
  template:
    spec:
      version: v1.32.0
      bootstrap:
        configRef:
          kind: KubeadmConfig
          name: mp-bench-azure-workers
      infrastructureRef:
        kind: AzureMachinePool
        name: mp-bench-azure-workers
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AzureMachinePool
metadata:
  name: mp-bench-azure-workers
spec:
  location: eastus
  strategy:
    type: RollingUpdate
    rollingUpdate:
      deletePolicy: Oldest
      maxSurge: 25%
      maxUnavailable: 1
  template:
    vmSize: Standard_D2s_v3
    osDisk:
      osType: Linux
      diskSizeGB: 30
      managedDisk:
        storageAccountType: Premium_LRS

Scale-Up Results

MetricMachineDeploymentMachinePoolAKS (managed)
First new node ready~2m 19s~1m 53s~1m 32s
All 15 new nodes ready~2m 26s~2m 47s~2m 37s
P50 per-node~2m 19s~2m 20s~1m 38s
P90 per-node~2m 26s~2m 47s~1m 43s

The headline: self-managed MachineDeployment and MachinePool land in the same ~2.5 minute ballpark. MachinePool got its first node ready ~25 seconds sooner, but MachineDeployment had all 15 new nodes ready ~20 seconds faster. The P50 is nearly identical at ~2m 20s.

AKS (AzureManagedMachinePool) was also benchmarked with the same 5 to 20 scale on Standard_D2s_v3. Here CAPI still declares desired replicas via MachinePool, but the node pool is fully managed by AKS; provisioning is handled by Azure’s managed control plane.

The cloud provisioning time dominates regardless of which CAPI primitive triggers it. Azure needs ~2-3 minutes to spin up a Standard_D2s_v3 VM either way. The arrival patterns differ based on who orchestrates the work, but the total time to full scale-out is roughly the same.

The Road Ahead

MachinePool graduated to Beta in v1.7 and is enabled by default. Azure and AWS have mature implementations; GCP supports managed pools for GKE. Dedicated area ownership for MachinePool was established in the CAPI project in order to move MachinePool from experimental to stable.

The most interesting development is in-place updates (Alpha in v1.12) - updating machines without deletion. Today this is supported for KubeadmControlPlane and MachineDeployment, bringing CAPI-managed in-place behavior to the primitives that track individual machines. MachinePool has always had in-place update capabilities when the underlying cloud scaling group supports it.

Conclusion

MachineDeployment is the default most teams reach for as it’s well-documented, GA, and works everywhere CAPI does. MachinePool, despite graduating to Beta in v1.7, remains underexplored. That’s a missed opportunity for teams running on hyperscalers.

As the benchmark showed, the cloud provisioning time dominates either way. What MachinePool does give you is a thinner abstraction: fewer objects in your management cluster, native scaling group features without reimplementing them in CAPI, and an architecture that aligns with how cloud providers already think about fleet management.

If you’re on Azure, AWS, or GKE and your worker nodes are interchangeable compute, MachinePool is worth evaluating. If you’re on vSphere, bare metal, or need per-machine lifecycle control, MachineDeployment remains the right choice. And with in-place updates now landing in CAPI v1.12, the gap between the two primitives continues to narrow, giving platform teams more flexibility regardless of which model they choose.

Resources

Recommended for You

Agents, MCP, and Kubernetes, Part 5

Agents, MCP, and Kubernetes, Part 5

Implementing behavioral guardrails for agents and MCP workflows on Kubernetes using layered policy enforcement and gateway-based governance.

Prashant Ramhit & Yevhen Khvastunov

Agents, MCP, and Kubernetes, Part 4

Agents, MCP, and Kubernetes, Part 4

Learn how to observe and operate AI agent platforms on Kubernetes using Istio Ambient Mode, Kiali, AgentGateway, and Prometheus, with practical guidance on performance, reliability, and governance.

Prashant Ramhit & Yevhen Khvastunov