Lesson  in  Kubelings — Learn Kubernetes the Rustlings Way

kubeadm: a cluster from three commands

The full kubeadm init → join runbook: what each phase does, the pod-network-cidr decision you can't take back, join tokens and their expiry, and the CNI step everyone forgets. Then run the join half for real — init tears node-02 out of the cluster, and you mint a fresh join command and bring it back Ready, the exact TLS-bootstrap flow a new node takes.

☁ iximiuz Labs only. kubeadm runs as root on real machines — host-level, outside the kubectl sandbox — so this can't run on your local kind cluster. Read the full init → join runbook below; then, since this playground is already a live cluster (you can't re-init one), the drill at the bottom exercises the join half for real: init tears worker node-02 out, and you bring it back with a fresh kubeadm join. Upgrades are deliberately not here — that's M8.7 (upgrade-runbook), same tool, different day.

What kubeadm is (and isn't)

Everything M7 taught — apiserver, etcd, scheduler, controller-manager, kubelet (7.4) — has to get onto machines somehow. kubeadm is the official minimal way: it bootstraps a control plane and joins nodes, generating the certificates, kubeconfigs, and static-pod manifests you've been reading all module. It does not install an OS, a container runtime, a CNI, or manage machines afterward (that's the platform layer — Cluster API, M10.5, drives kubeadm for you at fleet scale).

Prerequisites on every node, control plane or worker: a container runtime (containerd), kubelet + kubeadm + kubectl packages, swap off, and the br_netfilter/ip-forwarding sysctls the install docs list.

1 · kubeadm init — the control plane

kubeadm init --pod-network-cidr=10.244.0.0/16

That flag deserves its own sentence: it's the address space every pod IP comes from (M4.10's per-node slices are carved from it). It must not collide with your VPC/host networks, your CNI must agree with it (Flannel's manifest defaults to exactly 10.244.0.0/16), and changing it later is effectively a cluster rebuild. The one irreversible decision in the ceremony.

Watch the phase output scroll — it's this module in order:

[certs]           CA + component certs into /etc/kubernetes/pki
[kubeconfig]      admin.conf, kubelet.conf, … (cert-wrapped identities)
[etcd]            static-pod manifest for local etcd        ← 7.3's database
[control-plane]   apiserver/scheduler/controller-manager    ← static pods (7.4)
                  manifests into /etc/kubernetes/manifests/
[bootstrap-token] join tokens + the cluster-info ConfigMap
[addons]          CoreDNS (pending until CNI!) + kube-proxy DaemonSet

The kubelet was already running as a systemd service; it sees the manifest files appear and starts the control plane — nobody kubectl applys the apiserver; there's no API to apply it to yet. Chicken, meet static-pod egg.

2 · The kubeconfig copy (the step everyone fumbles once)

mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes
# control-plane   NotReady   control-plane   1m   v1.31.x

admin.conf is cluster-admin (M4.11 taught you to treat such files as standing credentials). And yes — NotReady is correct:

3 · The CNI step (why NotReady is by design)

M4.10 explained this exactly: no conflist in /etc/cni/net.d/ → kubelet reports NetworkPluginNotReady → node NotReady, CoreDNS Pending. Install a CNI whose pod CIDR matches step 1:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl get nodes -w     # NotReady → Ready as the DaemonSet lands

(Pin the manifest version in anything real — the course's own install policy.)

4 · kubeadm join — workers

init's last lines printed the ticket:

kubeadm join 10.0.0.10:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:1234…

Two credentials doing two jobs: the token proves the node may join (bootstrap-token auth, expires in 24h); the ca-cert-hash proves the node is joining the right cluster — it pins the CA so a MITM can't hand your kubelet a fake control plane. Lost the ticket? Mint a fresh one:

kubeadm token create --print-join-command

On the worker, join runs the short version of init's phases: discovery → TLS bootstrap (the kubelet requests a client cert via CSR — the Node,RBAC scoping from M6.8 §3 starts here) → kubelet.conf written → kubelet starts, CNI DaemonSet schedules onto it, node goes Ready.

kubectl get nodes
# control-plane   Ready    control-plane   12m
# worker-1        Ready    <none>          1m

Reading a bootstrap that went sideways

SymptomAlmost always
init hangs at [wait-control-plane]runtime dead or kubelet can't start static pods — journalctl -u kubelet, crictl ps -a
node stays NotReady after joinno/broken CNI (M4.10 triage table)
CoreDNS Pending foreversame — CNI
join fails with x509/token errorstoken expired (24h!) or wrong ca-cert-hash — mint a new join command
pods can't cross nodespod-network-cidr ↔ CNI manifest mismatch — the step-1 decision biting

kubeadm reset on a node undoes its half (then clean /etc/cni/net.d/ and iptables yourself — reset tells you what it left behind).

Takeaway

  • The ceremony is: init → copy kubeconfig → CNI → join ×N. Four moves, in that order, CNI is not optional.
  • --pod-network-cidr is the irreversible flag: match it to the CNI, keep it off your real networks.
  • Join security = token (may I?) + ca-cert-hash (is this really you?); tokens expire by design — kubeadm token create --print-join-command is the reflex.
  • Everything kubeadm writes, this module already taught you to read: /etc/kubernetes/pki, static-pod manifests, kubeconfigs. The magic is file placement.
  • CKA's Installation & Configuration domain (~25%) is this page plus the upgrade runbook (M8.7) and HA (next lesson).

Your turn

You can't kubeadm init a cluster that's already running — so this drill is the other half of the ceremony, the one you actually repeat in production: join. init ran kubeadm reset on worker node-02 and stopped its kubelet. From cplane-01 it now reads NotReady and then disappears altogether — no kubelet.conf, no PKI, no membership.

Bring it back Ready, using the real bootstrap-token flow:

  1. On cplane-01, drop the stale Node object first — kubectl delete node node-02. (A reset node's API object lingers as Ready; skip this and the join aborts with "a Node named node-02 already exists". Removing a dead node before re-provisioning is the real runbook.)
  2. Still on cplane-01, mint a join ticket — the printed command carries a fresh token and the CA cert hash: kubeadm token create --print-join-command.
  3. Run that kubeadm join … line on node-02, as root.
  4. Watch node-02 go from gone → NotReady → Ready as the CNI DaemonSet lands.

The check runs on node-02 and passes once it holds a freshly issuedkubelet.conf (newer than the reset) and the cluster reports it Ready.

    ┌─────────────┐     
    │kubeadm init │     
    │             │     
    └─────────────┘     
           │            
   control plane up     
           │            
           ▼            
 ┌─────────────────────┐
 │join token + CA hash │
 │                     │
 └─────────────────────┘
           │            
     worker enrols      
           │            
           ▼            
    ┌─────────────┐     
    │kubeadm join │     
    │             │     
    └─────────────┘     
Hint

Two machines, two roles — the same split the runbook describes.

First clear the corpse: the reset node still has a Ready Node object in the API (kubeadm reset can't always remove it, and node-02 has no admin credentials to do so itself). kubeadm join refuses to reuse the name:

# on cplane-01
kubectl delete node node-02

The token expires in 24h, so don't hunt for the one init printed when this cluster was built; mint a new one. --print-join-command hands you the whole line, both credentials already filled in:

# still on cplane-01
kubeadm token create --print-join-command
# -> kubeadm join <api>:6443 --token <fresh> --discovery-token-ca-cert-hash sha256:<hash>
# on node-02, as root — paste that exact line
kubeadm join <api>:6443 --token <fresh> --discovery-token-ca-cert-hash sha256:<hash>

You do not re-install a CNI: it's a DaemonSet that already exists cluster-wide and schedules onto node-02 the moment it registers — which is why NotReady flips to Ready on its own after a few seconds. Watch from cplane-01: kubectl get nodes -w.

Solution
# --- on cplane-01: remove the stale Node object, then mint a fresh join command
kubectl delete node node-02
kubeadm token create --print-join-command
# copy the printed 'kubeadm join …' line

# --- on node-02, as root: run exactly that line
kubeadm join 10.0.0.10:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:1234…
# [preflight] … [kubelet-start] … 'This node has joined the cluster'

# (a failed/partial join leaves state behind — 'kubeadm reset -f' on node-02
#  and try again if preflight complains about existing files)

# --- back on cplane-01: watch it come Ready (CNI DaemonSet lands on its own)
kubectl get nodes -w
# node-02   NotReady   <none>   5s    ->   Ready   <none>   25s

Root cause, restated

There was no failure to diagnose here — the point is muscle memory for the one kubeadm operation you genuinely repeat: adding a node. Everything that made it work is something the module already taught you to see. The token answered "may this machine join?" and the ca-cert-hash answered "is this the right cluster?" — two credentials, two questions. The node's kubelet then requested its own client cert via CSR (the TLS bootstrap, Node,RBAC scoped), wrote /etc/kubernetes/kubelet.conf, and started. And you touched the CNI exactly zero times: it's a DaemonSet, so it found the new node by itself — the same reason "NotReady until a CNI exists" from M4.10 runs in reverse here.