Control-Plane Integration¶
CAPTF’s cluster and machine modules are infrastructure only: they create networks, load balancers and instances, and report state back through the contract. Bringing up Kubernetes on top of that infrastructure is the job of a Cluster API control-plane provider — KubeadmControlPlane (KCP) or RKE2ControlPlane (RCP) — plus its bootstrap provider.
Those providers read specific fields from your modules and write specific fields back, on a specific schedule. Getting the details wrong produces a cluster that hangs rather than one that fails loudly.
This guide set distills the verified requirements into what you need as a module author:
-
KubeadmControlPlane
Everything specific to KubeadmControlPlane / the kubeadm bootstrap provider (CABPK).
-
RKE2ControlPlane
Everything specific to RKE2ControlPlane / the RKE2 bootstrap provider (CAPRKE2).
-
Requirements Checklist
Every port, health check and ordering rule from both guides, organized by topic, for a build-time or review-time reference.
Both guides cite CAPI/CAPRKE2 source paths and the contract’s own module role pages: the cluster role, the machine role and the common contract. Where this guide set and the contract state the same requirement, the contract’s wording is authoritative.
The shared creation sequence¶
Both control-plane providers drive the same shape of sequence against a CAPTF cluster module and one or more CAPTF machine modules. The sequence below is the kubeadm case; RKE2’s differences are called out inline and detailed in rke2.md, in what it reads from your modules and lifecycle constraints.
- A user (or a ClusterClass topology) creates the
Cluster,TerraformCluster, control-plane object (KubeadmControlPlaneorRKE2ControlPlane) andTerraformMachineTemplate. - The CAPI Cluster controller reconciles the
infrastructureRef, setting its owner reference to theTerraformCluster. - The
TerraformClustercontroller runs the cluster module’s first apply. At this pointcontrol_plane_endpointis renderednullandcontrol_plane_initializedis renderedfalse. The module returnscontrol_plane_endpoint,failure_domains,exportsandhealth. - The controller copies
control_plane_endpointontoTerraformCluster.spec.controlPlaneEndpointandfailure_domainsontoTerraformCluster.status.failureDomains; CAPI in turn copies them ontoCluster.spec.controlPlaneEndpointandCluster.status.initialization.infrastructureProvisioned = true. - The control-plane provider gates all further work on: a valid
Cluster.spec.controlPlaneEndpointandinfrastructureProvisioned = true. Neither KCP nor RCP creates a single Machine before both are true (see what KubeadmControlPlane reads and what RKE2ControlPlane reads). - For control-plane machine
i = 1..N, in strict order — the provider creates machinei+1only after machineihas anodeRef:- The control-plane provider creates the bootstrap config (init for
i=1, join fori>1) and theMachine(labeledcluster.x-k8s.io/control-plane) plus aTerraformMachinecloned from the template. - The bootstrap provider writes the bootstrap Secret (
value,format);Machine.spec.bootstrap.dataSecretNameis set. - The
TerraformMachinecontroller reconciles machinei, gated on: the cluster’sinfrastructureProvisioned, the cluster’sexportsbeing readable, and the bootstrap Secret existing. - The machine module’s apply runs once (immutable): it receives
bootstrap_data(base64),control_plane = true,captf_cluster_outputs(the cluster’sexports),failure_domainandkubernetes_version. In this same apply the module creates the instance and registers it in the control-plane LB backend, or does neither of the latter under Pattern C below. It returnsprovider_id,addresses,failure_domain,interruptibleandhealth. - The
TerraformMachinecontroller writesspec.providerIDandstatus.addresseson theTerraformMachine, and marks it provisioned andReady. The CAPI Machine controller then copies both ontoMachine.spec.providerIDandMachine.status.addresses. - cloud-init on the instance runs
kubeadm init(i=1) orkubeadm join(i>1); post-init phases go through the endpoint (hairpin — see kubeadm.md’s networking section). Under RKE2’s default registration method the join target is the endpoint host itself, but the join cannot proceed until at least one Ready control-plane Machine already exists, sinceRCP.status.availableServerIPsstays empty without one (see what RKE2ControlPlane reads and rke2.md’s lifecycle constraints). - The Node registers with
spec.providerIDset (by a cloud-controller-manager orkubelet --provider-id); the Machine controller matchesnodeRefby an exactproviderIDmatch. - When
i = 1and the control plane finishes initializing,Cluster.status.initialization.controlPlaneInitializedflipstrue. This triggers exactly one cluster re-apply, for resources gated on a live workload API server.
- The control-plane provider creates the bootstrap config (init for
- Workers follow the same machine path (
MachineDeployment→MachineSet→Machine→TerraformMachine,control_plane = false), gated onControlPlaneInitializedbecause the bootstrap provider writes worker join data only after that. A worker fleet backed by aMachinePoolinstead goes straight fromMachinePooltoTerraformMachinePool— one infrastructure object for the whole group, with no per-replicaMachineorTerraformMachine; see Machine Pools.
LB-membership patterns¶
The contract requires a control-plane machine module to register (and, on destroy, deregister) its instance with the control-plane load balancer (see machine.md’s control-plane machines), but leaves how up to the module. Three patterns are sanctioned here.
Pattern A: attachment resource in the machine module’s state¶
The machine module creates the instance and, in the same apply, an explicit attachment/registration resource (for example an LB target-group attachment) pointed at the target-group or backend-pool id it received through captf_cluster_outputs (see common.md’s outputs). Because the attachment lives in the machine module’s own state, destroy deregisters it as an ordinary part of tearing that state down.
This is the pattern the contract documents directly: registration is “done in the machine module’s own Terraform state, not the cluster module’s” (machine.md’s control-plane machines, citing capi/docs/book/src/developer/providers/contracts/infra-machine.md:633,645). The instance MUST be registered before kubeadm init/kubeadm join finishes (KCP) or before the Machine is Ready (RKE2) — see kubeadm.md’s module checklist and rke2.md’s module checklist.
Typical fit: clouds whose load balancer exposes an explicit target-group/backend-pool attach API (for example AWS ALB/NLB target-group attachments, Azure Load Balancer backend-pool membership).
Untested design guidance
This is design guidance, not yet tested against a real control plane.
Pattern B: selector/tag-based backend pools¶
The load balancer derives its backend membership itself, from a tag or label selector query, rather than from an explicit per-instance attach call — “target group by tag/label”. The machine module’s only job is to apply the right tags to the instance (via captf_tags, see common.md’s inputs, or an additional module-defined tag); the load balancer’s own membership scan does the rest, and removing the instance (destroy) removes it from the pool.
The same ordering requirement applies as under Pattern A: the instance MUST be a member of the backend before kubeadm init/join finishes on it (KCP) or before the Machine is Ready (RKE2) — see kubeadm.md’s module checklist and rke2.md’s module checklist.
Typical fit: clouds or load balancers whose backend pool is defined by a selector/tag query against an instance group or autoscaling group, rather than an explicit attach call.
Untested design guidance
This is design guidance, not yet tested against a real control plane.
Pattern C: kube-vip-style VIP¶
No separate load-balancer resource is created by the cluster module at all; a virtual IP is run by the control-plane nodes themselves (implicit via kube-vip or similar), so there is “nothing LB-wise” for the machine module to do. The cluster module still MUST emit a valid control_plane_endpoint before any Machine is created — the same gate applies regardless of how the endpoint is realized (see what KubeadmControlPlane reads and kubeadm.md’s networking section; what RKE2ControlPlane reads and rke2.md’s networking section). Under this pattern the backend-membership and per-backend health-check rows of checklist.md do not apply, because there is no separate LB backend to join.
Typical fit: bare-metal, on-premises, or otherwise L2-reachable environments without a managed load balancer in front of the control plane.
Untested design guidance
This is design guidance, not yet tested against a real control plane.