kubeadm_node¶
The shared node role: container runtime, kubelet, kubeadm binary, and the drain/uncordon/wait primitives. Everything a Kubernetes node needs regardless of whether it is a control plane or a worker.
This role is never named in site.yml
It is a meta/main.yml dependency of both kubeadm_server and kubeadm_agent, so it runs implicitly before either role's own tasks. Its individual task files are then re-entered during upgrades with include_role + tasks_from:.
If you are looking for "where does containerd get installed" and grepping site.yml, this is why you find nothing. See Architecture.
Task files¶
tasks/main.yml is short: four steps and an assertion:
| Step | File | Tags | Condition |
|---|---|---|---|
| Assert architecture is supported | inline | none | always |
| Detect installed versions and node state | detect-versions.yml |
always |
always |
| Install container runtime and kubelet | apply-node-runtime-and-kubelet.yml |
container_runtime, container_tools, k8s_install |
not kubeadm_node_bootstrapped |
| Install the kubeadm binary | apply-kubeadm-binary.yml |
k8s_install |
not kubeadm_node_bootstrapped |
| Pin kubelet to the node IP | pin-node-ip.yml |
always |
always |
The remaining eight files are not entered from main.yml. Five are pulled in by apply-node-runtime-and-kubelet.yml; three are reachable only through tasks_from::
| File | Called from |
|---|---|
configure-containerd.yml |
apply-node-runtime-and-kubelet.yml |
install-runc.yml |
apply-node-runtime-and-kubelet.yml |
install-cni.yml |
apply-node-runtime-and-kubelet.yml |
install-crictl.yml |
apply-node-runtime-and-kubelet.yml |
install-nerdctl.yml |
apply-node-runtime-and-kubelet.yml |
node-drain.yml |
kubeadm_agent/tasks/upgrade-node.yml |
node-uncordon.yml |
kubeadm_agent/tasks/upgrade-node.yml |
node-wait-ready.yml |
both upgrade paths |
detect-versions.yml and the facts it sets¶
This runs on every host, every time, under the always tag. Every gate in kubeadm_server and kubeadm_agent reads its output.
| Fact | Meaning |
|---|---|
kubeadm_node_bootstrapped |
The node has already joined a cluster |
kubeadm_node_upgrade_pending |
The installed version is behind the pinned target |
A fresh node has both false-ish, which is what makes the whole preflight and upgrade machinery no-op on a first install without needing a separate code path.
The eight tasks_from: call sites¶
flowchart LR
upcp["kubeadm_server<br/>upgrade-control-plane.yml"]:::aux
upnode["kubeadm_agent<br/>upgrade-node.yml"]:::aux
binary["apply-kubeadm-binary.yml"]
runtime["apply-node-runtime-and-kubelet.yml"]
wait["node-wait-ready.yml"]
drain["node-drain.yml"]
uncordon["node-uncordon.yml"]
upcp --> binary
upcp --> runtime
upcp --> wait
upnode --> binary
upnode --> drain
upnode --> runtime
upnode --> wait
upnode --> uncordon
classDef aux stroke:#78909c,stroke-dasharray:2 2
Three from the control plane, five from the worker. The payoff: installing a node and upgrading a node run the same task files, so the two paths cannot drift apart. The difference between them is exactly the drain and uncordon pair, which is the difference that matters.
Variables¶
| Variable | Purpose |
|---|---|
kubeadm_node_drain_timeout |
How long kubectl drain may take before failing |
kubeadm_node_wait_ready_retries |
Polls for the node to report Ready after an upgrade |
kubeadm_node_wait_ready_delay |
Seconds between polls |
kubeadm_node_node_ip |
Address kubelet registers as the Node InternalIP. node_ip from worker_hosts_json, else the host's default IPv4. Never empty |
kubeadm_node_kubelet_env_path |
/etc/default/kubelet, where the pin is reconciled. Survives kubeadm upgrade, unlike kubeadm-flags.env |
The node IP pin¶
pin-node-ip.yml writes KUBELET_EXTRA_ARGS="--node-ip=<addr>" on every pass, on the control plane and on every worker alike. It lives here rather than in kubeadm_agent because play 2 puts a tailscale0 on every cluster host, so the control plane needs the same guard a worker does. See kubeadm_agent for what goes wrong without it.
It runs before kubeadm init or kubeadm join, so the flag is in place the first time kubelet starts.
Handlers notified¶
Restart kubelet, in this role's own handlers/main.yml, notified by name when the node-IP pin changes /etc/default/kubelet. Role handler files load automatically, so no play imports it.
Cilium is deliberately left alone. It reads the tunnel endpoint from the Node object once at agent start, so changing a joined node's IP also needs kubectl -n kube-system rollout restart ds/cilium by hand. That is a cluster-wide action, not something one node's play should trigger.
Architecture detection¶
The role asserts the node architecture is supported, then keys every binary download off node_architecture (x86_64→amd64, aarch64→arm64). An ARM64 worker joining an AMD64 control plane gets ARM64 binaries automatically: containerd, runc, CNI, crictl, nerdctl, kubeadm and kubelet all resolve per host.
Re-run behaviour¶
Idempotent. apply-node-runtime-and-kubelet.yml and apply-kubeadm-binary.yml are gated on not kubeadm_node_bootstrapped, so on an existing node they are skipped entirely and only the upgrade path can touch them.
Gotchas¶
containerd needs SystemdCgroup = true
configure-containerd.yml sets it. If containerd and the kubelet disagree on the cgroup driver the kubelet starts and then fails to run pods, with an error that does not obviously point at cgroups.
detect-versions.yml is tagged always
That is deliberate. A tag-scoped run like --tags k8s_upgrade still needs the detection facts, so they must not be filtered out by the tag selector.