Installing a cluster with kubeadm

Get Ubuntu machines ready, build a control plane with kubeadm init, add a pod network and join the workers, step by step.

45 min 3 hands-on tasksArchitecture · 25% of the exam

Key ideas in this lesson · tap one for a simple explanation

What you will be able to do

  • Get a Linux machine ready for kubeadm: swap off, kernel modules and settings, containerd with the systemd cgroup driver, and the Kubernetes packages from pkgs.k8s.io held at one version.
  • Run kubeadm init with a pod network range, set up kubectl and install a network plugin (CNI).
  • Print a fresh join command, say what each part of it does and add a worker node.

is the setup tool that turns a few prepared Linux machines into a Kubernetes cluster. You run kubeadm init once, on the first machine, to build the . Then you run kubeadm join on every other machine to add it. kubeadm is not (your everyday command-line tool) and not the (the agent on every ); Control plane and node components keeps the three apart.

The exam rarely asks you to build a cluster from nothing. More often it gives you a half-prepared node, a worker to join, or a kubeadm error to read. This lesson covers two exam topics: Prepare underlying infrastructure for installing a Kubernetes cluster and Create and manage Kubernetes clusters using kubeadm.

Building a cluster in four steps

1. Prepare every machine

swap off, kernel settings, containerd

2. Install the packages

kubeadm, kubelet and kubectl

3. kubeadm init

builds the control plane, then add a network plugin

4. kubeadm join

adds each worker to the cluster

Steps 1 and 2 run on every machine. Step 3 runs once, on the first control plane machine. Step 4 runs on each worker.

Step 1: prepare every machine

Run these steps on every machine, control plane and workers alike. They turn swap off and switch on the kernel modules and settings that networking needs. Then they install , the .

Swap, kernel modules and sysctls (control plane and workers)
# 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-iptables
containerd with the systemd cgroup driver
sudo 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

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 containerd

Press Explain on each block to see what every line is for. Two steps trip people up most:

  • Swap must be off. By default the kubelet refuses to start on a machine with swap on. You can allow swap with failSwapOn: false, but on a kubeadm lab or exam node, just turn it off.
  • containerd and the kubelet must use the same cgroup driver. A cgroup driver is how Linux limits a container's CPU and memory. kubeadm sets the kubelet to systemd, so containerd must use systemd too. If they differ, control plane Pods keep restarting for no visible reason.

Step 2: install kubeadm, kubelet and kubectl

Install all three tools on every machine, at exactly the same version. Each pkgs.k8s.io repository serves only one minor version, such as 1.37. That is why the URL below contains v1.37, and why every upgrade starts by changing it (see Upgrading clusters).

kubeadm, kubelet and kubectl from pkgs.k8s.io
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 kubelet

After this step the kubelet restarts every few seconds. That is normal: it has no settings yet, until kubeadm init or kubeadm join writes them.

Step 3: kubeadm init and a network plugin

Run kubeadm init on the first machine only. It builds the control plane there: certificates, settings files and the for the , , the and the . (kubeadm init phase --help lists every step it runs.) It does not install a pod network. You add one right after, and until then the node stays NotReady.

kubeadm init, kubeconfig, CNI (control plane node only)
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                  # Ready

Pick a pod network range that does not overlap. The range in --pod-network-cidr must not overlap your machines' network or the range. For example, if your VMs have addresses like 192.168.1.20, then 192.168.0.0/16 clashes with them. Use --pod-network-cidr=10.244.0.0/16 instead, and set CALICO_IPV4POOL_CIDR in calico.yaml to the same value before you apply it.

Step 4: join the workers

A worker joins with one command, kubeadm join, run on the worker itself. The control plane prints that command for you. It has three parts:

Part of the join commandWhat it isExample
The addressWhere the worker finds the API server: an IP or name, and port 6443.10.0.0.10:6443
--tokenA bootstrap token: a short-lived password that lets a new node join. By default it expires after 24 hours.abcdef.0123456789abcdef
--discovery-token-ca-cert-hashA fingerprint of the cluster CA's public key. The worker checks it, so it cannot be tricked into joining a fake cluster.sha256: and 64 hex characters
How a worker joins

Control plane

kubeadm token create --print-join-command

The join command

address, token and CA hash

Worker

runs it and joins the cluster

sudo kubeadm join …

Join the workers
# 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 wide

In the official docs· open on exam day too

Hands-on task 1 of 3ar-kubeadm-1
~20 min8% weightArchitecture

Prepare a node and bootstrap a control plane

Node 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.

Runs on a kubeadm cluster (VM lab), not on the kind lab.

This one needs a cluster built with kubeadm. Read it and study the solution now, then run it on your VM lab.
  1. 1

    Start from a healthy lab

    No setup needed for this one.

  2. 2

    Solve it in your terminal

    Use kubectl and the Kubernetes docs, like on exam day. Save any answer files under /tmp/cka/.

  3. 3

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-1/verify | bash
    Got FAIL? Read which check failed, fix it, run it again.
Hands-on task 2 of 3ar-kubeadm-2
~8 min5% weightArchitecture

Join a worker with a fresh token

Node 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.

Runs on a kubeadm cluster (VM lab), not on the kind lab.

This one needs a cluster built with kubeadm. Read it and study the solution now, then run it on your VM lab.
  1. 1

    Start from a healthy lab

    No setup needed for this one.

  2. 2

    Solve it in your terminal

    Use kubectl and the Kubernetes docs, like on exam day. Save any answer files under /tmp/cka/.

  3. 3

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-2/verify | bash
    Got FAIL? Read which check failed, fix it, run it again.
Hands-on task 3 of 3ar-kubeadm-3
~6 min4% weightArchitecture

Work out the join parameters without a new token

A new worker will join this cluster with a bootstrap token that the security team hands out separately. Prepare the other two parts of its 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.

Cluster:kubectl config use-context kind-cka

  1. 1

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/setup | bash
  2. 2

    Solve it in your terminal

    Use kubectl and the Kubernetes docs, like on exam day. Save any answer files under /tmp/cka/.

  3. 3

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/verify | bash
    Got FAIL? Read which check failed, fix it, run it again.
  4. 4

    Clean up (optional)

    curl -fsSL https://codewithmuh.com/labs/cka/t/ar-kubeadm-3/cleanup | bash

Finished this lesson?

0 of 3 tasks passed. Your readiness counts tasks, so run them before you move on.