This guide walks through bootstrapping a Kubernetes cluster with kubeadm. The
flow is: prepare every host identically, initialize the first control plane node,
install a pod network (CNI), and finally join additional control plane and worker
nodes.
Prerequisites (on every host)#
These steps must run on every node, both control plane and workers, because the kubelet expects the same base environment everywhere.
Disable swap memory#
Kubernetes requires swap to be off (by default). The scheduler makes placement decisions based on real available memory; if the kernel can silently page memory to disk, those guarantees break and the kubelet refuses to start. We disable it both at runtime and persistently so it stays off after a reboot.
# disable swap
sudo swapoff -a
# disable swap persistent (remove form /etc/fstab)
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
free -h # swap should be 0Install containerd#
Kubernetes does not run containers itself. It talks to a container runtime
through the CRI (Container Runtime Interface). containerd is a lightweight,
reliable, CRI-native runtime and the common default.
The key detail is the cgroup driver: both the kubelet and the runtime must
manage cgroups the same way. On modern systemd-based distros that means
SystemdCgroup = true. A mismatch here leads to nodes that become unstable under
load. We also pin the pause (sandbox) image, the tiny container that holds the
Linux namespaces shared by all containers in a pod.
# update packages in apt package manager
sudo apt update
# install containerd using the apt package manager
# containerd is lightwieght, reliable and fast (CRI native)
sudo apt-get install -y containerd
# create /etc/containerd directory for containerd configuration
sudo mkdir -p /etc/containerd
# Generate the default containerd configuration
# Change the pause container to version 3.10 (pause container holds the linux ns for Kubernetes namespaces)
# Set `SystemdCgroup` to true to use same cgroup drive as kubelet
containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' | sed 's|sandbox_image = ".*"|sandbox_image = "registry.k8s.io/pause:3.10"|' | sudo tee /etc/containerd/config.toml > /dev/null
# Restart containerd to apply the configuration changes
sudo systemctl restart containerdNetwork configuration#
The Linux kernel does not forward packets between interfaces by default. Pod
networking relies on this forwarding so traffic can be routed between pods and
nodes, so we enable ip_forward and persist it across reboots via a sysctl
drop-in file.
# sysctl params required by setup, params persist across reboots
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
# Apply sysctl params without reboot
sudo sysctl --systemInstall kubeadm, kubelet, and kubectl#
These are the three core tools:
- kubeadm bootstraps and joins clusters.
- kubelet is the node agent that runs on every host and starts pods.
- kubectl is the CLI used to talk to the cluster.
We add the official Kubernetes apt repository (signed with its GPG key for
package authenticity) and then apt-mark hold the packages so an unattended
apt upgrade can’t skip a minor version and break the cluster. Kubernetes
upgrades must be done deliberately, one minor version at a time.
# Update the `apt` package index and install packages needed to use the Kubernetes `apt` repository:
sudo apt update
# apt-transport-https may be a dummy package; if so, you can skip that package
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
# Download the public signing key for the Kubernetes package repositories. The same signing key is used for all repositories so you can disregard the version in the URL:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command.
sudo mkdir -p -m 755 /etc/apt/keyrings
sudo curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the appropriate Kubernetes `apt` repository. Please note that this repository have packages only for Kubernetes 1.36; for other Kubernetes minor versions, you need to change the Kubernetes minor version in the URL to match your desired minor version (you should also check that you are reading the documentation for the version of Kubernetes that you plan to install).
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/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
# (Optional) Enable the kubelet service before running kubeadm
sudo systemctl enable --now kubeletInitialize the cluster (only from the first control plane)#
Run this only on the first control plane node. kubeadm init generates the
cluster’s certificates, starts the control plane components (API server,
controller manager, scheduler, etcd), and prints the kubeadm join commands you
will need for the other nodes.
Pre-pulling images first makes the init faster and surfaces any registry/network
problems early. The --pod-network-cidr must match what your CNI expects, and
192.168.0.0/16 is Calico’s default. The kubeadm reset line is only needed
when you are retrying a failed init on an already-touched node.
# pull images
sudo kubeadm config images pull
# execute before every new try
sudo kubeadm reset --cri-socket=unix:///run/containerd/containerd.sock -f
# initialize controlplane
sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --cri-socket=unix:///run/containerd/containerd.sock
# HOW TO RESET IF NEEDED
# sudo kubeadm reset --cri-socket=unix:///run/containerd/containerd.sock
# sudo rm -rf /etc/kubernetes /var/lib/etcd After init succeeds, copy the admin kubeconfig into your home directory so
kubectl can authenticate as a regular (non-root) user. The admin.conf holds
the client certificate that grants cluster-admin access.
# ONLY ON CONTROL PLANE (also in the output of 'kubeadm init' command)
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# set alias
alias k=kubectl
# copy kubeadm join command!!Tip: Save the
kubeadm joincommand from the init output now. You’ll need it (with a fresh token) to add worker and control plane nodes later.
Install a Container Network Interface (CNI)#
Right after init, nodes show as NotReady and pods stay Pending because there
is no pod network yet. The CNI plugin provides pod-to-pod networking across nodes.
Here we use Calico, installed via its Tigera operator. Once the Calico pods
are running, the nodes flip to Ready.
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/custom-resources.yaml
# wait for the pods to be ready
k get po -A -w Join the cluster with another control plane#
For a highly available (HA) control plane you add more control plane nodes.
Unlike a worker, a new control plane needs a copy of the cluster certificates, so
you first re-upload them and get a short-lived certificate key. Join tokens
also expire (24h by default), so generate a fresh one with token create.
HA note: A multi control plane setup requires a stable
controlPlaneEndpoint(a load balancer / VIP in front of the API servers). If it wasn’t set at init time, add it to thekubeadm-configConfigMap in thekube-systemnamespace, e.g.controlPlaneEndpoint: 10.9.137.195:6443.
# create certificate key
sudo kubeadm init phase upload-certs --upload-certs
sudo kubeadm token create --print-join-command
# add the following entry to the kubeadm-config ConfigMap in the kube-system namespace: controlPlaneEndpoint: 10.9.137.195:6443
kubeadm join 192.168.0.200:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--control-plane --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxJoin the cluster with worker nodes#
Worker nodes run your actual workloads. They only need to authenticate to the API
server, so the join command is simpler and needs just the token and the CA cert
hash (the hash lets the node verify it’s talking to the right cluster). No certificate key
and no --control-plane flag here.
kubeadm join 10.9.137.162:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
