RKE2ControlPlane
What RKE2ControlPlane (RCP) and the RKE2 bootstrap provider (CAPRKE2) require of your CAPTF cluster and machine modules, and why this is the case.
Citations below prefixed rke2/ are paths in
rancher/cluster-api-provider-rke2 at tag v0.25.2; rke2docs/ is
rancher/rke2-docs (main branch); capi/ is the CAPI v1.14.2 tree
(https://github.com/kubernetes-sigs/cluster-api/tree/v1.14.2).
See also the shared creation sequence
and the requirements checklist for every requirement in
one place.
Version and compatibility
CAPRKE2 v0.25.2 is built against sigs.k8s.io/cluster-api v1.13.5
(rke2/go.mod:33) and its v1beta2 API is the contract version CAPTF also
targets. Its own e2e suite runs only against CAPI core v1.12.11 and
v1.13.5 (rke2/test/e2e/config/e2e_conf.yaml:22,33), and its
getting-started guide pins CAPI core to v1.13.5
(rke2/docs/book/src/01_user/01_getting-started.md:56).
CAPTF runs CAPI v1.14.2. The contract shape is unchanged, but CAPI v1.14 core is untested by CAPRKE2 v0.25.2 — run a compatibility smoke test before relying on it in production; see open questions.
What RKE2ControlPlane reads from your modules
-
Cluster.status.initialization.infrastructureProvisioned. RCP creates nothing — not even certificates — until this istrue; RKE2Config generates no bootstrap data until it istrueeither (rke2/controlplane/internal/controllers/rke2controlplane_controller.go:376-394;rke2/bootstrap/internal/controllers/rke2config_controller.go:171-182). -
Cluster.spec.controlPlaneEndpoint. A hard gate: RCP creates no Machine at all — including the first — while the endpoint is not valid (host and port both set) (rke2/controlplane/internal/controllers/rke2controlplane_controller.go:421-440). Only the endpoint’s host feeds the RKE2tls-sanlist and the init node’sServerURL(https://<host>:9345); the endpoint’s port is ignored for the supervisor join URL, which is always 9345 (rke2/pkg/rke2/config.go:350;rke2/bootstrap/internal/controllers/rke2config_controller.go:66,455-473). -
Cluster.spec.clusterNetwork.pods/services.cidrBlocksmap tocluster-cidr/service-cidrwhen non-empty (rke2/pkg/rke2/config.go:209-215).serviceDomainandapi_server_portare not read at all: cluster domain comes from RKE2’s ownserverConfig.clusterDomain, and the API server is always on 6443 on the node (rke2/pkg/rke2/config.go:224-225). Do not derive an LB backend port fromapi_server_portunder RKE2 beyond using it as the frontend port if your templates choose to. -
Failure domains. Same rule as KCP: only entries with
controlPlane: trueare used for scale-up/scale-down placement; a nilcontrolPlanecounts asfalse(rke2/pkg/rke2/control_plane.go:176-190). -
Machine.status.addresses. Read only for three of the fiveregistrationMethodvalues, and only from Machines whoseReadycondition is alreadyTrue(theregistrationMethodtable below;rke2/controlplane/internal/controllers/status.go:80,152,158-177). The contract’s canonical output order foraddresses(InternalIP, InternalDNS, ExternalIP, ExternalDNS, Hostname — see../contract/v1alpha1/machine.mdaddresses) is what makes RCP’s first-match logic deterministic; your module’s job is only to emit the address type(s) each method requires (below), not to order them — the controller does that. -
Machine.status.nodeRef. Used for etcd leader-move/member-removal ordering and remediation;HasHealthyMachineStillProvisioning= a healthy Machine with no Node yet (rke2/pkg/rke2/workload_cluster_etcd.go:40-45;rke2/pkg/rke2/control_plane.go:527-529). -
registrationMethod(immutable once set, enforced by webhook):Method status.availableServerIPs=Address types your module must supply Join URL control-plane-endpoint(default,""≡ this)[endpoint.host]none https://<endpoint host>:9345address[spec.registrationAddress]none https://<registrationAddress>:9345internal-firstfirst InternalIPorExternalIPper Ready CP Machine, in list orderInternalIPand/orExternalIPhttps://<ip>:9345internal-only-ipsfirst InternalIPper Ready CP MachineInternalIPhttps://<ip>:9345external-only-ipsfirst ExternalIPper Ready CP MachineExternalIPhttps://<ip>:9345(
rke2/pkg/registration/registration.go:46,68-140;rke2/controlplane/api/v1beta2/rke2controlplane_types.go:97-100;rke2/controlplane/api/v1beta2/rke2controlplane_webhook.go:142-145,211-214.) A Ready Machine with no address of the required type produces the RCP status error “ready but they have no IP Address available” (rke2/controlplane/internal/controllers/status.go:175-177); see open questions. -
RKE2ControlPlane.spec.version. Must match(v\d\.\d{2}\.\d+\+rke2r\d)|^$and is copied verbatim toMachine.spec.version(rke2/controlplane/api/v1beta2/rke2controlplane_types.go:79-81;rke2/controlplane/internal/controllers/scale.go:566-567,612). See../contract/v1alpha1/machine.mdkubernetes_versionand the module checklist.
What RCP and RKE2Config write
- Bootstrap Secret. Name = RKE2Config name; keys
valueandformatare always both written (there is no case whereformatis absent);formatdefaults tocloud-config(rke2/bootstrap/internal/controllers/rke2config_controller.go:1040-1061;rke2/bootstrap/api/v1beta2/rke2config_webhook.go:79-80). See../contract/v1alpha1/machine.mdbootstrap_format. - Ignition is supported end to end (init, CP join, worker), via
Butane → Ignition 3.3
(
rke2/bootstrap/internal/controllers/rke2config_controller.go:552-556,798-802,927-931). gzipUserData: true. For cloud-config,valuebecomes raw gzip bytes (not base64, not UTF-8 text);formatstayscloud-config. For Ignition, the gzip is wrapped inside the Ignition config instead (rke2/bootstrap/internal/controllers/rke2config_controller.go:1015-1037). See machine.md’sbootstrap_datainput for how the contract resolves the binary-payload question, and the module checklist for what it means for your module.- Cluster Secrets.
<cluster>-ca,-cca(client CA),-peer-etcd,-etcd,-kubeconfig,-token. No-sa, no-proxy(contrast with KCP’s secret set, kubeadm.md’s what KCP and CABPK write) (rke2/pkg/secret/certificates.go:52-80,168-190,382-384). - Machine labels/annotations. Template labels plus forced
cluster.x-k8s.io/cluster-name,cluster.x-k8s.io/control-plane: ""; apre-terminate.delete.hook.machine.cluster.x-k8s.io/rke2-cleanupannotation on every control-plane Machine (rke2/controlplane/internal/controllers/scale.go:650-666,577-578). - RCP status (v1beta2).
initialization.controlPlaneInitialized(true once the workloadkube-system/rke2-servingSecret exists, or once every owned Machine is Ready);availableServerIPs; conditions includingEtcdClusterHealthy,ControlPlaneComponentsHealthy,Remediating(rke2/controlplane/internal/controllers/status.go:98-194;rke2/controlplane/api/v1beta2/rke2controlplane_types.go:262-316). - Node identity. CAPRKE2 never sets kubelet
provider-id,node-ipornode-nameitself; the fields exist in its config but nothing in the repo writes them (rke2/pkg/rke2/config.go:501-503). See the module checklist and machine.md’s node providerID matching — RKE2.
Lifecycle constraints
- Init. Exactly one control-plane Machine gets init data, guarded by
an init-lock ConfigMap. The init node has no
server:configured — it creates the cluster standalone and, notably, needs no LB/9345 reachability to itself during init, unlike the hairpin requirement that applies to every later join (rke2/controlplane/internal/controllers/scale.go:51-99;rke2/pkg/rke2/config.go:650-676). Don’t read this as weakening the general endpoint-reachability requirement in networking and load balancer: it applies to every Machine that joins, i.e. every control-plane Machine after the first, and to every worker. - Join (control-plane and worker), in order. Requires the Cluster’s
ControlPlaneInitializedcondition; the<cluster>-tokenSecret; andRCP.status.availableServerIPsnon-empty, which itself needs at least one Ready control-plane Machine and a reachable workload API server via the<cluster>-kubeconfigSecret (rke2/bootstrap/internal/controllers/rke2config_controller.go:230,699-703,852-856;rke2/controlplane/internal/controllers/status.go:114-146,152). Both CP joins and worker joins useavailableServerIPs[0](rke2/bootstrap/internal/controllers/rke2config_controller.go:717,867). - Rollout.
maxSurge0 or 1, default 1; scale-up then scale-down of the oldest outdated Machine in the most-populated failure domain (rke2/controlplane/api/v1beta2/rke2controlplane_types.go:548-580;rke2/controlplane/internal/controllers/scale.go:285-334). - Preflight (scale up/down, in-place). No Machine deleting; every
control-plane Machine must have
AgentHealthyandEtcdMemberHealthybothTrue(Unknownor missing blocks); a missing Node blocks by extension (rke2/controlplane/internal/controllers/scale.go:200-266). - Scale-down / deletion. Etcd leadership is forwarded to the newest
Machine, then the Machine is deleted; the
rke2-cleanuppre-terminate hook re-forwards leadership, annotates the Node for etcd removal, and waits for confirmation before releasing — InfraMachine deletion happens only after that (rke2/controlplane/internal/controllers/scale.go:141-197). - Remediation. RCP remediates control-plane Machines with
HealthCheckSucceeded=False+OwnerRemediated=False(MHC-driven). Pre-init remediation is allowed directly; post-init remediation additionally requires more than one replica, no Machine still provisioning without a Node, no Machine deleting, and preserved etcd quorum (rke2/controlplane/internal/controllers/remediation.go:97-330,366-420). Machine.md’s health → remediation section already reflects this: remediation happens for Machines whose owner acts on the MachineHealthCheck’sMachineOwnerRemediatedcondition, and RCP is such an owner for its own control-plane Machines, not only MachineSet and KubeadmControlPlane. - In-place updates. Behind the CAPI
InPlaceUpdatesfeature gate (alpha) plus exactly one registeredCanUpdateMachineextension; otherwise falls back to delete/recreate. Not something a CAPTF module needs to support, sinceTerraformMachine.spec.sourceis immutable regardless (rke2/controlplane/main.go:287). - Payload size / air-gap. Init control-plane user-data embeds four CA
key pairs plus config and optional manifest/registry/audit files.
Non-air-gapped installs
curl -sfL https://get.rke2.ioat boot, which means node internet egress;airGapped: trueexpects pre-baked artifacts in the image instead (rke2/bootstrap/internal/cloudinit/controlplane_init.go:34-36).gzipUserDataexists for size-limited clouds; see machine.md’sbootstrap_datainput for how the contract handles the resulting binary payload.
Networking and load balancer
| Path | Port | Needed when |
|---|---|---|
| LB/VIP → CP nodes | 6443/TCP (endpoint port → node 6443; RKE2 ignores apiServerPort) | always |
| LB/VIP → CP nodes | 9345/TCP supervisor, same host as the endpoint | control-plane-endpoint (default) and address registration methods |
| All nodes → CP nodes | 6443, 9345 TCP direct (after registration) | always |
| CP ↔ CP | 2379, 2380, 2381 TCP | embedded etcd (not with externalDatastoreSecret) |
| all ↔ all | 10250 TCP; NodePort range (default 30000-32767) | always |
| CNI (default canal) | 8472/UDP VXLAN, 9099/TCP | cni: canal or unset |
(rke2/controlplane/internal/controllers/rke2controlplane_controller.go:757;
rke2docs/docs/install/ha.md:42; rke2docs/docs/install/requirements.md:128,142-148,158-161.)
- No LB listener on 2379 is needed. The controller reaches etcd only
by port-forwarding through the API server; an LB entry for 2379 is not
required by anything in RCP
(
rke2/pkg/proxy/dial.go:99-106;rke2/pkg/etcd/client_generator.go:34). - Health checks. 9345: TLS
GET /v1-rke2/readyzexpecting 403 (unauthenticated) — or plain TCP. 6443: TCP, or HTTPS/healthzonly if anonymous auth is enabled (rke2/examples/templates/docker/cluster-template.yaml:60,176-194). - DNS endpoints are supported. RKE2 HA supports a DNS name /
round-robin DNS as the fixed registration address; the endpoint host is
added to
tls-sanautomatically.registrationAddressis not added totls-sanautomatically — add it viaserverConfig.tlsSanif it differs from the endpoint host (rke2docs/docs/install/ha.md:36-37;rke2/pkg/rke2/config.go:350). - Backend membership. The module owns LB membership — nothing in RCP or CAPI registers a backend for you. The instance MUST be added before its Machine is Ready (joins happen through the LB during bring-up), and the LB MUST tolerate a backend whose supervisor is not yet up (see lifecycle constraints, “Join”).
Module checklist
Cluster module
- MUST output a valid
control_plane_endpoint(host and port) no later than the apply that makes the clusterprovisioned, unless the user sets one — RCP creates no Machine at all untilCluster.spec.controlPlaneEndpoint.IsValid()(rke2/controlplane/internal/controllers/rke2controlplane_controller.go:421-440; see what RKE2ControlPlane reads). This matches the contract’s own endpoint-timing rule in../contract/v1alpha1/cluster.mdcontrol_plane_endpointoutput. - MUST keep the emitted
control_plane_endpointstable for the life of the object: CAPI never updatesCluster.spec.controlPlaneEndpointafter the first valid copy (capi/core/reconcilers/cluster/cluster_controller_phases.go:217-228), so replacing the LB behind it (new DNS name or IP) breaks every kubeconfig and the API server certificate SANs. The controller neither detects nor prevents such a replacement;lifecycle { prevent_destroy = true }on the resource behind the endpoint turns it into a failed Job instead of a lost cluster (../contract/v1alpha1/cluster.mdcontrol_plane_endpointoutput). - MUST provision two LB listeners on the same host:
endpoint.port→ CP nodes:6443, and:9345→ CP nodes:9345— not just one, as with KCP (rke2docs/docs/install/ha.md:42; see networking and load balancer). - MUST NOT rely on
cluster_network.api_server_portorservice_domainto configure RKE2; RKE2 reads neither (rke2/pkg/rke2/config.go:224-225; see what RKE2ControlPlane reads). - Backend membership is the module’s job (not CAPI’s): a target group by tag/label, or an explicit attach resource — see the three patterns in README.md’s LB-membership patterns.
- SHOULD leave
failure_domains[].control_planeat its defaulttrue(rke2/pkg/rke2/control_plane.go:176-190; see what RKE2ControlPlane reads), same as under KCP.
Machine module
- MUST accept both
bootstrap_formatvalues RCP can write,cloud-configandignition(rke2/bootstrap/internal/controllers/rke2config_controller.go:1040-1061; see what RCP and RKE2Config write). - MUST register the control-plane instance in both the 6443 and 9345 LB
target sets, and open/attach the corresponding security rules, before
the Machine becomes Ready
(
capi/docs/book/src/developer/providers/contracts/infra-machine.md:633,645; see networking and load balancer). - MUST supply
addressesof the type(s) the cluster’sregistrationMethodneeds (theregistrationMethodtable in what RKE2ControlPlane reads) when that method is anything other thancontrol-plane-endpoint/address; the controller, not the module, is responsible for output ordering (../contract/v1alpha1/machine.mdaddresses). - MUST emit
provider_idmatching exactly what the Node’s kubelet registers — either via a cloud-controller-manager, or viaagentConfig.kubelet.extraArgs: [provider-id=<value>]set from instance metadata, since CAPRKE2 sets none of this itself (see what RCP and RKE2Config write). Under RKE2 a mismatch on the first control-plane Machine blocks every subsequent join, not just that Machine’s own readiness, because every join needsavailableServerIPsnon-empty, which needs a Ready CP Machine (see what RKE2ControlPlane reads and lifecycle constraints). kubernetes_versionarrives asvX.Y.Z+rke2rN; strip the+rke2rNsuffix before using it for an image lookup or a semver comparison (see what RKE2ControlPlane reads).- Init/CP-join payloads embed four CA key pairs; on size-limited clouds
both
cloud-config+gzipUserData: trueandignition+gzipUserData: truework for reducing payload size (see what RCP and RKE2Config write). Either way the module MUST routebootstrap_datato a base64-taking argument (e.g.user_data_base64); it MUST NOTbase64decode()it, because a gzipped payload is not valid UTF-8.
Differences from KubeadmControlPlane
| Aspect | KubeadmControlPlane | RKE2ControlPlane v0.25.2 |
|---|---|---|
| LB ports | API port only | API (6443) and supervisor 9345, same host |
| Join target | CP endpoint, backend port | availableServerIPs[0]:9345 — endpoint host, registrationAddress, or a CP Machine IP |
Uses Machine.status.addresses | no | yes, for 3 of 5 registrationMethod values |
| Join gate | Cluster ControlPlaneInitialized | that, plus ≥1 Ready CP Machine (needs Node ⇒ providerID match) |
Machine.spec.version | vX.Y.Z | vX.Y.Z+rke2rN |
clusterNetwork.apiServerPort | not wired to bindPort either, but the field exists as a convention (see kubeadm.md’s what it reads) | ignored entirely; 6443 is fixed |
clusterNetwork.serviceDomain | honored by CABPK | ignored (serverConfig.clusterDomain) |
Bootstrap format | always present (cloud-config or ignition) | always present |
| Secrets | -ca, -etcd, -sa, -proxy, -kubeconfig | -ca, -cca, -etcd, -peer-etcd, -kubeconfig, -token |
| etcd removal | KCP pre-terminate hook, etcd client | rke2-cleanup pre-terminate hook, Node annotation etcd.rke2.cattle.io/remove |
| Install | kubeadm/kubelet pre-installed in image | curl get.rke2.io at boot unless air-gapped artifacts are baked in |
| Contract | v1beta2, built on CAPI v1.14 | v1beta2, built on CAPI v1.13.5 |
(Individual rows cited in the corresponding sections above and in
kubeadm.md.)
Open questions
- No
InternalIPguarantee.addressesmay legally be[](../contract/v1alpha1/machine.mdaddresses). Forinternal-only-ips/external-only-ips/internal-first, a Ready Machine without the right address type blocks every subsequent join with an RCP status error (see what RKE2ControlPlane reads). Notfcapi-lintrule warns when a module never emits anInternalIP. - CAPI v1.14 compatibility. CAPRKE2 v0.25.2 is tested only up to CAPI core v1.13.5 (see version and compatibility). CAPTF runs v1.14.2. Run a compatibility smoke test against v1.14.2 before relying on RKE2ControlPlane in production; watch for a CAPRKE2 release built on v1.14.
- Nondeterministic join target. Under IP-based
registrationMethodvalues,availableServerIPsis built by iterating a Go map, so which control-plane node a joiner targets is nondeterministic, and a stale or just-removed node’s address can be chosen until the next status refresh (see what RKE2ControlPlane reads;rke2/bootstrap/internal/controllers/rke2config_controller.go:717,867). A module cannot fix this directly; a Machine that leavesReady(for example through a MachineHealthCheck) drops out ofavailableServerIPson the next status refresh.
See also
README.md— the shared creation sequence and the LB-membership patterns.kubeadm.md— the same requirements under KubeadmControlPlane.checklist.md— every requirement from both guides in one place.- The machine role — the normative contract this guide builds on.