Document

SUBSCRIBE TO GET FULL ACCESS TO THE E-BOOKS FOR FREE 🎁SUBSCRIBE NOW

Professional Dropdown with Icon

SUBSCRIBE NOW TO GET FREE ACCESS TO EBOOKS

KubernetesLeçon 3 / 357 min de lectureMis à jour le September 10, 2026

kubeadm is the official tool for bootstrapping a Kubernetes cluster that follows upstream best practices. Unlike managed services (EKS, AKS, GKE) it shows you every component, which is exactly what you need to understand Kubernetes and to pass the CKA exam. This tutorial builds a cluster with one control plane and two workers on Ubuntu 24.04, using containerd as the runtime, the current pkgs.k8s.io package repositories, Kubernetes 1.33 and Calico as the pod network. Older guides that use apt-key, apt.kubernetes.io or Docker as runtime no longer work; everything below has been checked against the 2026 tooling.

Prerequisites: three Ubuntu 24.04 machines (VMs or cloud instances) with at least 2 vCPU and 2 GB RAM each (4 GB recommended for the control plane), unique hostnames, static IPs on the same network, sudo access, and open ports: 6443, 2379–2380, 10250, 10257, 10259 on the control plane; 10250 and 30000–32767 on workers; plus TCP 179 and IP-in-IP (protocol 4) for Calico on all nodes.

Lab layout

RoleHostnameIP (example)
Control planecp1192.168.56.10
Workerw1192.168.56.11
Workerw2192.168.56.12

The pod network CIDR will be 192.168.0.0/16 (Calico’s default). If your node network already uses 192.168.x.x, pick 10.244.0.0/16 instead and adjust both kubeadm init and the Calico configuration.

Step 1 – Prepare all three nodes

Run this section on every node. Kubernetes requires swap to be off, and the kernel must forward IPv4 traffic and let iptables see bridged traffic.

# Hostnames (adapt per node)
sudo hostnamectl set-hostname cp1
echo "192.168.56.10 cp1
192.168.56.11 w1
192.168.56.12 w2" | sudo tee -a /etc/hosts

# Disable swap now and at boot
sudo swapoff -a
sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab

# Kernel modules and sysctl
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay && sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system

Step 2 – Install containerd

Kubernetes talks to the runtime through CRI; containerd is the standard choice. The one setting that trips everyone is the cgroup driver: kubelet defaults to systemd, so containerd must too.

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

# Use the systemd cgroup driver and the current pause image
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo sed -i 's#sandbox_image = .*#sandbox_image = "registry.k8s.io/pause:3.10"#' /etc/containerd/config.toml

sudo systemctl restart containerd
sudo systemctl enable containerd
systemctl status containerd --no-pager | head -3

Step 3 – Install kubeadm, kubelet and kubectl

Packages come from the community-owned pkgs.k8s.io repositories, one repository per minor version. The signing key is stored with signed-by; apt-key is gone.

K8S_MINOR=v1.33
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:/${K8S_MINOR}/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:/${K8S_MINOR}/deb/ /" \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl     # upgrades are done deliberately with kubeadm
sudo systemctl enable --now kubelet

kubeadm version -o short

To install a specific patch, list versions with apt-cache madison kubeadm and pin, for example, kubelet=1.33.4-1.1 kubeadm=1.33.4-1.1 kubectl=1.33.4-1.1. Kubelet will crash-loop until kubeadm init or join gives it a configuration; that is expected.

Step 4 – Initialise the control plane (cp1 only)

sudo kubeadm init \
  --pod-network-cidr=192.168.0.0/16 \
  --apiserver-advertise-address=192.168.56.10 \
  --control-plane-endpoint=cp1 \
  --kubernetes-version=stable-1.33

--control-plane-endpoint is optional today but lets you add more control-plane nodes later without re-initialising. After a minute the command prints a success message with a kubeadm join line: copy it. Then configure kubectl for your user:

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
# NAME   STATUS     ROLES           AGE   VERSION
# cp1    NotReady   control-plane   60s   v1.33.4

The node is NotReady because no pod network exists yet; CoreDNS pods stay Pending for the same reason.

Step 5 – Install Calico

Calico is installed through its operator: first the operator and CRDs, then a Installation resource describing the network. Check the Calico quickstart for the latest version number and replace it below.

CALICO_VERSION=v3.30.2
kubectl create -f "https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VERSION}/manifests/tigera-operator.yaml"

# Custom resources: only edit cidr if you changed --pod-network-cidr
curl -fsSLO "https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VERSION}/manifests/custom-resources.yaml"
grep cidr custom-resources.yaml            # should show 192.168.0.0/16
kubectl create -f custom-resources.yaml

# Wait until every Calico component is Available
watch kubectl get tigerastatus
# Excerpt of custom-resources.yaml – the part you may need to change
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
      - name: default-ipv4-ippool
        blockSize: 26
        cidr: 192.168.0.0/16
        encapsulation: VXLANCrossSubnet
        natOutgoing: Enabled
        nodeSelector: all()

Once tigerastatus shows apiserver, calico and ippools as Available, the control plane turns Ready and CoreDNS starts running.

Step 6 – Join the workers (w1 and w2)

# Paste the join command printed by kubeadm init, e.g.:
sudo kubeadm join cp1:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:<HASH_FROM_INIT_OUTPUT>

# Lost the command? Generate a fresh one on cp1 (tokens expire after 24 h):
kubeadm token create --print-join-command

Step 7 – Verify the cluster

kubectl get nodes -o wide
# NAME   STATUS   ROLES           AGE   VERSION   INTERNAL-IP     OS-IMAGE             CONTAINER-RUNTIME
# cp1    Ready    control-plane   12m   v1.33.4   192.168.56.10   Ubuntu 24.04.2 LTS   containerd://1.7.27
# w1     Ready    <none>          3m    v1.33.4   192.168.56.11   ...
# w2     Ready    <none>          2m    v1.33.4   192.168.56.12   ...

kubectl get pods -A                    # all Running
kubectl label node w1 w2 node-role.kubernetes.io/worker=   # cosmetic role label

# Smoke test: deploy nginx, expose it, curl it from a node
kubectl create deployment web --image=nginx:1.27 --replicas=2
kubectl expose deployment web --port=80 --type=NodePort
kubectl get svc web                    # note the 3xxxx NodePort
curl -s http://192.168.56.11:<NODEPORT> | grep -o '<title>.*</title>'

# Pod-to-pod DNS across nodes
kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- nslookup web.default.svc.cluster.local

Optional: run workloads on the control plane

Single-node labs need to remove the control-plane taint so ordinary pods can schedule there:

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

Troubleshooting

  • kubeadm init fails on preflight “swap is enabled” – re-run swapoff -a; on cloud images also mask swap.img.swap.
  • Pods stuck in ContainerCreating / “cgroup driver mismatch”SystemdCgroup = true was not applied; check /etc/containerd/config.toml and restart containerd, then kubeadm reset -f and start again.
  • Node NotReady forever – Calico not running: kubectl get pods -n calico-system, kubectl describe tigerastatus calico. Common cause: firewall blocking BGP (179) or VXLAN (UDP 4789).
  • Join fails with certificate hash mismatch – token expired or copied badly; generate a new join command.
  • Everything broken, start over – on the node: sudo kubeadm reset -f && sudo rm -rf /etc/cni/net.d $HOME/.kube.

Upgrading later

Because the packages are on hold, upgrades are explicit: change K8S_MINOR in the repository file, apt-get update, unhold and install the new kubeadm, run kubeadm upgrade plan then kubeadm upgrade apply v1.34.x on the control plane, and finally upgrade kubelet/kubectl node by node after draining each one. The full procedure is in Upgrading Kubernetes.

Key takeaways

  • 2026 stack: Ubuntu 24.04, containerd with the systemd cgroup driver, pkgs.k8s.io repos, Kubernetes 1.33, Calico via operator.
  • kubeadm init on the control plane, kubeadm join on workers; hold the packages and upgrade deliberately.
  • Nodes are NotReady until a CNI plugin is installed; that is by design.
  • Always verify with a real deployment, a NodePort and a DNS lookup across nodes.

Next tutorial

Next: etcd, the cluster datastore and CNI plugins compared. Official docs: Creating a cluster with kubeadm, Container runtimes.