Installing a cluster with kubeadm
Prepare Ubuntu nodes for Kubernetes, bootstrap a control plane with kubeadm init, add a pod network and join workers, every command in the order you run it.
After this lesson
- 01You can prepare a Linux host for kubeadm: swap, kernel modules, sysctls, containerd with the systemd cgroup driver, and pinned packages from pkgs.k8s.io.
- 02You can run kubeadm init with a pod network CIDR, set up kubectl and install a CNI plugin.
- 03You can produce a fresh join command, explain each part of it and add a worker node.
This lesson covers two competencies: Prepare underlying infrastructure for installing a Kubernetes cluster and Create and manage Kubernetes clusters using kubeadm. The exam rarely makes you build a cluster from nothing, but it does hand you a node that is half prepared, a worker that needs joining, or a kubeadm preflight error to read. kind runs this same procedure inside containers for you, so practice it on two Ubuntu 24.04 VMs (Multipass, Vagrant or any cloud) with 2 CPUs and 2 GiB of RAM or more. Every command runs as a user with sudo.
Prepare every node
# Swap off now and after reboots
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Kernel modules, loaded now and at boot
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
# sysctls, persisted and applied now
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptablessudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
# The runc option lives under [plugins.'io.containerd.cri.v1.runtime'...runc.options] in containerd 2.x
# and [plugins."io.containerd.grpc.v1.cri"...runc.options] in 1.7. The default config prints it either way.
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep SystemdCgroup /etc/containerd/config.toml # SystemdCgroup = true
sudo systemctl restart containerd
sudo systemctl enable containerdWhy each step is there. net.ipv4.ip_forward = 1 is the setting Kubernetes itself needs, and kubeadm's preflight stops when it is off. br_netfilter and the two bridge-nf-call sysctls make traffic crossing a Linux bridge visible to iptables, which kube-proxy's iptables mode and most CNI plugins rely on; those sysctls only exist once the module is loaded. overlay backs containerd's default snapshotter. Swap support exists, but the kubelet still refuses to start on a node with swap unless you set failSwapOn: false, so on a kubeadm lab or exam node turn it off. Last, the kubelet and the runtime must agree on the cgroup driver: kubeadm configures the kubelet for systemd, so runc has to use it too. A mismatch shows up as control plane Pods that restart for no visible reason.
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
apt-cache madison kubeadm | head -3 # package versions look like 1.37.1-1.1
VER=1.37.1-1.1 # pick one from the list
sudo apt-get install -y kubelet=$VER kubeadm=$VER kubectl=$VER
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubeletEach pkgs.k8s.io repository serves exactly one minor version. That is why the URL contains v1.37, and why every upgrade starts by editing this file. The old apt.kubernetes.io and yum.kubernetes.io repositories have been shut down, so any guide that uses them is out of date. apt-mark hold keeps a routine apt upgrade from moving the cluster under you. After this step the kubelet restarts every few seconds because it has no configuration yet; that is expected until kubeadm init or kubeadm join writes one. Network-wise, the control plane needs TCP 6443 (API server), 2379-2380 (etcd), 10250 (kubelet), 10257 and 10259 (controller manager and scheduler) reachable; workers need 10250, 10256 (kube-proxy health) and the NodePort range 30000-32767.
Bootstrap the control plane
sudo kubeadm init \
--pod-network-cidr=192.168.0.0/16 \
--apiserver-advertise-address=10.0.0.10 \
--kubernetes-version=v1.37.1
# kubectl for your user; kubeadm prints these lines when it finishes
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes # NotReady: no pod network yet
kubectl -n kube-system get pods # coredns Pending for the same reason
# A CNI plugin. Calico's manifest defaults to 192.168.0.0/16.
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/calico.yaml
kubectl -n kube-system rollout status ds/calico-node --timeout=300s
kubectl get nodes # Readykubeadm init runs in phases you can list with kubeadm init phase --help: preflight checks, certificates under /etc/kubernetes/pki, kubeconfig files for the admin, kubelet, controller manager and scheduler, static Pod manifests for etcd and the control plane, then the kubeadm-config ConfigMap, the control-plane taint, a bootstrap token, and the CoreDNS and kube-proxy add-ons. A CNI is not one of them; until you install one, the node stays NotReady and CoreDNS stays Pending. The pod CIDR must not overlap the node or Service networks. If your VMs live on 192.168.x.x, initialize with --pod-network-cidr=10.244.0.0/16 and set CALICO_IPV4POOL_CIDR in calico.yaml to the same value before you apply it.
# On the control plane. The token from kubeadm init expires after 24 hours, so mint a fresh command.
sudo kubeadm token create --print-join-command
# kubeadm join 10.0.0.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>
# On each worker, after the node preparation above
sudo kubeadm join 10.0.0.10:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
# The hash pins the cluster CA's public key. You can compute it yourself on the control plane:
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
# Token housekeeping
sudo kubeadm token list
sudo kubeadm token delete <token-id>
kubectl get nodes -o wideIn the docs · the tab you get on exam day
- Installing kubeadm ↗search: “installing kubeadm”
- Container Runtimes ↗search: “container runtimes systemd cgroup”
- Creating a cluster with kubeadm ↗search: “create cluster kubeadm”
- kubeadm join ↗search: “kubeadm join”
Cluster: a kubeadm-built cluster (VM lab). The kind lab cannot run this one.
Prepare a node and bootstrap a control plane
cp1 is a fresh Ubuntu 24.04 VM with 2 CPUs and 4 GiB of RAM. Prepare it for Kubernetes: swap off, the kernel modules and sysctls a cluster needs, containerd using the systemd cgroup driver, and kubelet, kubeadm and kubectl v1.37 from pkgs.k8s.io held at that version. Then initialize a control plane with pod network CIDR 192.168.0.0/16, make kubectl work for the user you are logged in as, and install Calico v3.33.0 so that cp1 becomes Ready.This one needs a cluster you built with kubeadm. Read it and the solution now; run it when you have the VM lab.
- 01
Nothing to set up
Start on a healthy lab. Check it with lab-check.sh if unsure.
- 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-1/verify | bashCounts toward your readiness score.
Cluster: a kubeadm-built cluster (VM lab). The kind lab cannot run this one.
Join a worker with a fresh token
w1 has been prepared like cp1 (containerd, v1.37 packages, kubelet enabled) but is not part of the cluster yet, and the token printed by kubeadm init has expired. Join w1 to the cluster as a worker. You can reach it from cp1 with ssh w1.This one needs a cluster you built with kubeadm. Read it and the solution now; run it when you have the VM lab.
- 01
Nothing to set up
Start on a healthy lab. Check it with lab-check.sh if unsure.
- 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-2/verify | bashCounts toward your readiness score.
context · kubectl config use-context kind-cka
Work out the join parameters without a new token
kubeadm join command. Write the API server endpoint the join must target, as host:port exactly as the cluster publishes it for joining nodes, to /tmp/cka/ar-join-endpoint.txt. Write the value for --discovery-token-ca-cert-hash, including its sha256: prefix, to /tmp/cka/ar-join-hash.txt. Do not create or delete any bootstrap tokens.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/cleanup | bash
Finished reading?
0 of 3 tasks passed. Readiness counts tasks, so run them before you move on.