Skip to main content
In this lesson you’ll build a simple Kubernetes cluster with kubeadm across three Ubuntu VMs: one control-plane node and two worker nodes. The steps follow the official Kubernetes documentation:
  • Bootstrapping Clusters with kubeadm → Creating a Cluster with kubeadm
  • Installing kubeadm / kubelet / kubectl
This guide shows the exact commands to run on each node and explains why each step is needed. It uses:
  • Ubuntu 22.04 LTS VMs (controlplane, node01, node02)
  • containerd as the container runtime
  • Calico as the CNI plugin
  • kubeadm/kubelet/kubectl installed from the official apt repository
Before you begin: follow the official kubeadm docs while performing the steps:

Environment (used in this demo)


Overview — Main steps

  1. Install required packages and prerequisites.
  2. Load kernel modules and set sysctl params required by Kubernetes.
  3. Install and configure containerd with SystemdCgroup = true.
  4. Install kubeadm, kubelet, and kubectl.
  5. Ensure kubelet binds to the node IP used for cluster traffic.
  6. Initialize the control-plane with kubeadm init.
  7. Install a CNI plugin (Calico in this lesson).
  8. Join worker nodes to the cluster.
  9. Validate with an NGINX Deployment and a NodePort service.

1 — Install basic utilities on all nodes

Run on controlplane, node01, and node02:
These utilities are required to add repositories, fetch manifests, and download packages.

2 — Load kernel modules and set sysctl parameters

Kubernetes requires packet forwarding and bridge network settings. Apply these on all nodes:
These settings ensure iptables sees bridged traffic and IPv4 forwarding is enabled.

3 — Install and configure containerd

Install containerd and configure it to use systemd cgroups (recommended):
Verify the setting:
Why SystemdCgroup = true? Kubelet commonly uses the systemd cgroup driver on modern Kubernetes setups. Matching containerd avoids a cgroup driver mismatch between container runtime and kubelet.

4 — Install kubeadm, kubelet and kubectl

Add the Kubernetes apt repository and install the packages on all nodes:
Optional: install crictl/cri-tools to inspect CRI containers:
crictl is helpful for troubleshooting container runtime issues when Kubernetes API is unreachable.

5 — Ensure kubelet binds to the correct node IP (multi-interface environments)

In multi-interface VMs kubelet might pick the wrong IP. If your environment exposes a PRIMARY_IP environment variable per VM (pointing to the intended cluster interface), configure kubelet to use it: On each node (controlplane, node01, node02):
Verify PRIMARY_IP (example):
If you don’t have PRIMARY_IP, determine the correct interface with ip addr / ip route and replace ${PRIMARY_IP} with that IP.

6 — Initialize the control-plane node

Run kubeadm init on the control-plane node only. Provide the API server advertise address and the pod network CIDR that matches your CNI (Calico expects you to configure its ipPool accordingly). Example:
On success kubeadm prints:
  • Commands to configure kubectl for your user (copy /etc/kubernetes/admin.conf)
  • The kubeadm join ... command for worker nodes
Configure kubectl for the current (non-root) user:
Check the control plane status (note: the controlplane node may be NotReady until a CNI is installed):
Protect the admin.conf and the kubeadm join token. Anyone with these can gain access to the cluster. If a token is lost, recreate it with sudo kubeadm token create --print-join-command.

7 — Install the CNI plugin (Calico example)

Calico needs to know the pod CIDR. Download the Calico custom resources and set the ipPool CIDR to match the POD_CIDR used during kubeadm init (10.244.0.0/16 in this example): On the control-plane node:
Depending on the Calico release you may also need to apply the main Calico manifests; follow the official docs: https://docs.tigera.io/calico/latest/getting-started/on-premises/. Wait until Calico and core system pods are healthy:

8 — Join worker nodes

When kubeadm init completed it printed a kubeadm join command. If you lost it, recreate it on the control-plane node:
Run the printed kubeadm join command with sudo on each worker node (node01, node02). Example (your token/hash will differ):
Back on the control-plane node, verify all nodes are Ready:
If a node is briefly NotReady, wait for the CNI pods to be scheduled and become healthy.

9 — Quick test: deploy NGINX and expose it via NodePort

Create a Deployment and expose it via a NodePort (example uses node port 30143):
Find the node IPs:
From any node or a machine that can reach the nodes, curl the NodePort:
If the NGINX page is reachable via the node IP and NodePort, cluster networking and services are functioning correctly.

Troubleshooting notes and exam tips

  • Recreate a lost join command on the control-plane node:
  • If nodes remain NotReady, check:
    1. containerd is running and configured with systemd cgroups (SystemdCgroup = true in /etc/containerd/config.toml),
    2. kubelet is bound to the correct --node-ip,
    3. the CNI is installed and its pod network CIDR matches the one passed to kubeadm init.
  • For certification exams: you will have the official docs available. Practice navigating:

This completes the kubeadm-based cluster installation and validation. You can substitute a different container runtime or CNI plugin: the kubeadm init/join flow remains the same — only the runtime or network plugin installation steps differ.

Watch Video