kubeadm - Bootstrap a Kubernetes Cluster from Scratch¶
- In this lab we will learn how to use kubeadm to bootstrap a fully functional Kubernetes cluster. We will set up a control-plane node, join worker nodes, install a CNI plugin, and verify the cluster is operational.
What will we learn?¶
- What
kubeadmis and its role in the Kubernetes ecosystem - How to prepare nodes (control-plane and workers) for cluster creation
- How to initialize a control-plane node with
kubeadm init - How to join worker nodes with
kubeadm join - How to install a Container Network Interface (CNI) plugin
- How to configure
kubectlfor the new cluster - How to upgrade a cluster using
kubeadm upgrade - How to reset and tear down a cluster with
kubeadm reset - Best practices for production-grade cluster bootstrapping
Official Documentation & References¶
| Resource | Link |
|---|---|
| kubeadm Overview | kubernetes.io/docs |
| Creating a cluster with kubeadm | kubernetes.io/docs |
| kubeadm init | kubernetes.io/docs |
| kubeadm join | kubernetes.io/docs |
| kubeadm upgrade | kubernetes.io/docs |
| kubeadm reset | kubernetes.io/docs |
| Installing kubeadm | kubernetes.io/docs |
| Container Runtimes | kubernetes.io/docs |
| CNI Plugins | kubernetes.io/docs |
Prerequisites¶
- Two or more Linux machines (physical or virtual) - one for the control-plane and one or more for workers
- Each machine must have at least 2 CPUs and 2 GB RAM (recommended)
- Full network connectivity between all machines
- Unique hostname, MAC address, and
product_uuidfor each node - Swap disabled on all nodes
- A supported container runtime installed (containerd, CRI-O, or Docker with cri-dockerd)
Lab Environment Options
You can run this lab using:
- Multipass VMs on macOS/Linux
- Vagrant + VirtualBox or libvirt
- Cloud VMs (AWS EC2, GCP Compute Engine, Azure VMs)
- Docker containers for a quick local experiment (see the included
setup-k8s.sh) - LXD containers on Linux
For this lab, instructions are given for Ubuntu/Debian-based systems. Adapt package commands as needed for other distributions.
kubeadm Architecture Overview¶
graph TB
subgraph control["Control-Plane Node"]
api["kube-apiserver"]
etcd["etcd"]
sched["kube-scheduler"]
cm["kube-controller-manager"]
kubelet_cp["kubelet"]
cni_cp["CNI Plugin"]
end
subgraph worker1["Worker Node 1"]
kubelet_w1["kubelet"]
proxy_w1["kube-proxy"]
cni_w1["CNI Plugin"]
pods_w1["Pods"]
end
subgraph worker2["Worker Node 2"]
kubelet_w2["kubelet"]
proxy_w2["kube-proxy"]
cni_w2["CNI Plugin"]
pods_w2["Pods"]
end
api <--> etcd
api <--> sched
api <--> cm
kubelet_cp --> api
kubelet_w1 --> api
kubelet_w2 --> api
style control fill:#326CE5,color:#fff
style worker1 fill:#4a9,color:#fff
style worker2 fill:#4a9,color:#fff | Component | Role |
|---|---|
kubeadm init | Bootstraps the control-plane node |
kubeadm join | Joins a worker (or additional control-plane) node to the cluster |
kubeadm upgrade | Upgrades the cluster to a newer Kubernetes version |
kubeadm reset | Tears down the cluster on a node (undo init or join) |
kubeadm token | Manages bootstrap tokens for node joining |
kubeadm certs | Manages cluster certificates |
01. Prepare all nodes¶
Run these steps on every node (control-plane and workers).
01.01 Disable swap¶
Kubernetes requires swap to be disabled:
# Disable swap immediately
sudo swapoff -a
# Disable swap permanently (comment out swap in fstab)
sudo sed -i '/ swap / s/^/#/' /etc/fstab
01.02 Load required kernel modules¶
# Load modules needed by containerd/Kubernetes networking
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
01.03 Set sysctl parameters for Kubernetes networking¶
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
# Apply without reboot
sudo sysctl --system
01.04 Install the container runtime (containerd)¶
# Install containerd
sudo apt-get update
sudo apt-get install -y containerd
# Generate default config
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# Enable SystemdCgroup (required for kubeadm)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart containerd
sudo systemctl restart containerd
sudo systemctl enable containerd
Why containerd?
- Since Kubernetes 1.24, dockershim was removed from
kubelet. - The recommended container runtimes are containerd or CRI-O.
- If you need Docker CLI tools, install Docker separately - it will use containerd under the hood.
01.05 Install kubeadm, kubelet, and kubectl¶
# Install prerequisites
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
# Add the Kubernetes apt repository signing key
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the Kubernetes apt repository
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
# Install kubeadm, kubelet, and kubectl
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
# Pin their versions to prevent accidental upgrades
sudo apt-mark hold kubelet kubeadm kubectl
# Add the Kubernetes yum repository
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.31/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.31/rpm/repodata/repomd.xml.key
EOF
# Install kubeadm, kubelet, and kubectl
sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# Enable kubelet service
sudo systemctl enable kubelet
01.06 Enable the kubelet service¶
Note
At this point the kubelet will crash-loop every few seconds - this is expected. It is waiting for kubeadm init or kubeadm join to provide its configuration.
02. Initialize the control-plane node¶
Run these steps only on the control-plane node.
02.01 Run kubeadm init¶
sudo kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--apiserver-advertise-address=<CONTROL_PLANE_IP>
Replace Placeholders
Replace <CONTROL_PLANE_IP> with the actual IP address of your control-plane node. The --pod-network-cidr must match the CIDR expected by your CNI plugin (10.244.0.0/16 for Flannel, 192.168.0.0/16 for Calico).
Expected output (excerpt):
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Then you can join any number of worker nodes by running the following
on each as root:
kubeadm join <CONTROL_PLANE_IP>:6443 --token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH>
02.02 Configure kubectl¶
# Set up kubectl for the current user
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
02.03 Verify control-plane status¶
# The node should appear with status NotReady (until CNI is installed)
kubectl get nodes
# Check that control-plane pods are running
kubectl get pods -n kube-system
Expected output:
NAME READY STATUS RESTARTS AGE
coredns-xxxxxxxxxx-xxxxx 0/1 Pending 0 1m
coredns-xxxxxxxxxx-xxxxx 0/1 Pending 0 1m
etcd-control-plane 1/1 Running 0 1m
kube-apiserver-control-plane 1/1 Running 0 1m
kube-controller-manager-control-plane 1/1 Running 0 1m
kube-proxy-xxxxx 1/1 Running 0 1m
kube-scheduler-control-plane 1/1 Running 0 1m
Note
CoreDNS pods will stay in Pending state until a CNI plugin is installed. This is normal.
03. Install a CNI plugin¶
A CNI (Container Network Interface) plugin is required so pods can communicate across nodes. Choose one of the following:
Note
Flannel requires --pod-network-cidr=10.244.0.0/16 to be set during kubeadm init.
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
Note
If you used --pod-network-cidr=192.168.0.0/16 during kubeadm init, Calico works with zero additional config. For custom CIDRs, edit the CALICO_IPV4POOL_CIDR environment variable in the manifest.
# Install Cilium CLI
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
curl -L --fail --remote-name-all \
https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz
# Install Cilium into the cluster
cilium install
After installing the CNI, verify all pods come up:
# Wait for all system pods to be running
kubectl get pods -n kube-system -w
# Node should now show Ready
kubectl get nodes
Expected output:
04. Join worker nodes¶
Run these steps on each worker node.
04.01 Use the join command from kubeadm init output¶
sudo kubeadm join <CONTROL_PLANE_IP>:6443 \
--token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH>
Forgot the join command?
If you lost the join command, generate a new token on the control-plane node:
04.02 Verify nodes from the control-plane¶
Expected output:
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 10m v1.31.x
worker-1 Ready <none> 2m v1.31.x
worker-2 Ready <none> 1m v1.31.x
04.03 (Optional) Label worker nodes¶
kubectl label node worker-1 node-role.kubernetes.io/worker=worker
kubectl label node worker-2 node-role.kubernetes.io/worker=worker
05. Verify the cluster¶
05.01 Deploy a test application¶
kubectl create deployment nginx-test --image=nginx --replicas=3
kubectl expose deployment nginx-test --port=80 --type=NodePort
05.02 Check deployment status¶
# All 3 replicas should be running across the nodes
kubectl get pods -o wide
# Get the NodePort
kubectl get svc nginx-test
05.03 Test connectivity¶
# Get the NodePort assigned
NODE_PORT=$(kubectl get svc nginx-test -o jsonpath='{.spec.ports[0].nodePort}')
# Test from any node (replace with an actual node IP)
curl http://<NODE_IP>:${NODE_PORT}
05.04 Run a cluster health check¶
# Check component status
kubectl get componentstatuses 2>/dev/null || kubectl get --raw='/readyz?verbose'
# Check all namespaces
kubectl get pods --all-namespaces
# Verify DNS resolution
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- \
nslookup kubernetes.default.svc.cluster.local
05.05 Clean up the test deployment¶
06. (Optional) Allow scheduling on the control-plane¶
By default, the control-plane node has a taint that prevents workload pods from being scheduled on it. For single-node clusters or development environments, remove it:
Warning
Do not do this in production. The control-plane node should be dedicated to running control-plane components for stability and security.
07. Managing bootstrap tokens¶
# List existing tokens
kubeadm token list
# Create a new token (default TTL: 24h)
kubeadm token create
# Create a token with a custom TTL
kubeadm token create --ttl 2h
# Create a token and print the full join command
kubeadm token create --print-join-command
# Delete a specific token
kubeadm token delete <TOKEN>
08. Upgrade the cluster¶
To upgrade a cluster from one Kubernetes minor version to the next:
08.01 Upgrade the control-plane¶
# Update the Kubernetes repo to the target version (e.g., v1.32)
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
# Update apt and upgrade kubeadm
sudo apt-get update
sudo apt-mark unhold kubeadm
sudo apt-get install -y kubeadm
sudo apt-mark hold kubeadm
# Verify kubeadm version
kubeadm version
# Check the upgrade plan
sudo kubeadm upgrade plan
# Apply the upgrade (replace with actual target version)
sudo kubeadm upgrade apply v1.32.0
08.02 Upgrade kubelet and kubectl on the control-plane¶
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet kubectl
sudo apt-mark hold kubelet kubectl
# Restart kubelet
sudo systemctl daemon-reload
sudo systemctl restart kubelet
08.03 Upgrade worker nodes¶
On each worker node:
# Drain the node (run from the control-plane)
kubectl drain <WORKER_NODE> --ignore-daemonsets --delete-emptydir-data
# On the worker node: upgrade kubeadm, then kubelet + kubectl
sudo apt-mark unhold kubeadm kubelet kubectl
sudo apt-get update
sudo apt-get install -y kubeadm kubelet kubectl
sudo apt-mark hold kubeadm kubelet kubectl
# Upgrade the node configuration
sudo kubeadm upgrade node
# Restart kubelet
sudo systemctl daemon-reload
sudo systemctl restart kubelet
08.04 Verify the upgrade¶
Expected output:
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 1d v1.32.0
worker-1 Ready worker 1d v1.32.0
worker-2 Ready worker 1d v1.32.0
09. Certificate management¶
kubeadm manages cluster certificates automatically. Here are useful commands:
# Check certificate expiration dates
sudo kubeadm certs check-expiration
# Renew all certificates
sudo kubeadm certs renew all
# Renew a specific certificate
sudo kubeadm certs renew apiserver
Certificate Validity
By default, kubeadm certificates are valid for 1 year. The CA certificate is valid for 10 years. Plan certificate renewal before expiration to avoid cluster downtime.
10. Using a kubeadm configuration file¶
Instead of passing many command-line flags, you can use a configuration file:
# manifests/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.31.0
controlPlaneEndpoint: "control-plane:6443"
networking:
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/12"
dnsDomain: "cluster.local"
apiServer:
extraArgs:
- name: audit-log-path
value: /var/log/kubernetes/audit.log
- name: audit-log-maxage
value: "30"
etcd:
local:
dataDir: /var/lib/etcd
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
nodeRegistration:
criSocket: unix:///var/run/containerd/containerd.sock
taints:
- key: "node-role.kubernetes.io/control-plane"
effect: "NoSchedule"
Generating a Default Config
You can generate a default configuration to customize:
11. Reset and tear down¶
To completely remove Kubernetes from a node:
# Reset the node (removes all cluster state)
sudo kubeadm reset -f
# Clean up networking rules and CNI configs
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X
sudo rm -rf /etc/cni/net.d
# Remove kubeconfig
rm -rf $HOME/.kube
# (Optional) Uninstall packages
sudo apt-mark unhold kubelet kubeadm kubectl
sudo apt-get purge -y kubelet kubeadm kubectl
Summary¶
| Concept | Key Takeaway |
|---|---|
| kubeadm init | Bootstraps a control-plane node with all required components |
| kubeadm join | Adds worker (or HA control-plane) nodes to the cluster |
| CNI plugin | Required for pod networking - install immediately after kubeadm init |
| kubeadm upgrade | Safely upgrades cluster version one minor release at a time |
| kubeadm reset | Cleanly tears down cluster state on a node |
| kubeadm token | Manages tokens for joining nodes (default 24h TTL) |
| kubeadm certs | Manages TLS certificates (default 1-year validity) |
| Swap must be off | kubelet will not start if swap is enabled |
| Container runtime | containerd or CRI-O required (dockershim removed since K8s 1.24) |
| Config file | --config flag allows declarative cluster configuration |
Exercises¶
The following exercises will test your understanding of kubeadm and cluster bootstrapping. Try to solve each exercise on your own before revealing the solution.
01. Create a single-node cluster that can run workloads¶
Create a cluster with kubeadm init that allows scheduling pods on the control-plane node.
Solution
# Initialize the cluster
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
# Set up kubeconfig
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# Install CNI (Flannel)
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
# Remove the control-plane taint to allow scheduling
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
# Verify the node is Ready
kubectl get nodes
02. Generate a new join token and add a worker node¶
The original join token from kubeadm init has expired. Generate a new one and join a worker.
Solution
# On the control-plane node - generate a new token with the full join command
kubeadm token create --print-join-command
# On the worker node - run the printed command, e.g.:
sudo kubeadm join 192.168.1.100:6443 \
--token abc123.abcdefghijklmnop \
--discovery-token-ca-cert-hash sha256:abc123...
# On the control-plane - verify the worker joined
kubectl get nodes
03. Check and renew cluster certificates¶
Check when the cluster certificates expire and renew the API server certificate.
Solution
# Check expiration dates for all certificates
sudo kubeadm certs check-expiration
# Renew only the API server certificate
sudo kubeadm certs renew apiserver
# Restart the API server to pick up the new cert
# (API server runs as a static pod, so restart kubelet or move the manifest)
sudo systemctl restart kubelet
# Verify the new certificate
sudo kubeadm certs check-expiration | grep apiserver
04. Perform a cluster upgrade from v1.31 to v1.32¶
Plan and execute a full cluster upgrade across control-plane and worker nodes.
Solution
# ---- Control-Plane Node ----
# 1. Update the apt repo to v1.32
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
# 2. Upgrade kubeadm
sudo apt-get update
sudo apt-mark unhold kubeadm
sudo apt-get install -y kubeadm
sudo apt-mark hold kubeadm
# 3. Check the plan
sudo kubeadm upgrade plan
# 4. Apply the upgrade
sudo kubeadm upgrade apply v1.32.0
# 5. Upgrade kubelet and kubectl
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet kubectl
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet
# ---- Each Worker Node ----
# 6. Drain the worker (from control-plane)
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
# 7. On the worker: upgrade all components
sudo apt-mark unhold kubeadm kubelet kubectl
sudo apt-get update
sudo apt-get install -y kubeadm kubelet kubectl
sudo apt-mark hold kubeadm kubelet kubectl
# 8. Upgrade the node config
sudo kubeadm upgrade node
# 9. Restart kubelet
sudo systemctl daemon-reload
sudo systemctl restart kubelet
# 10. Uncordon the worker (from control-plane)
kubectl uncordon worker-1
# Verify
kubectl get nodes
Troubleshooting¶
-
kubelet crash-loops after
Common causes: swap is enabled, cgroup driver mismatch, containerd not running.kubeadm init: -
CoreDNS pods stuck in Pending: A CNI plugin must be installed. CoreDNS cannot schedule without a pod network.
-
Node remains NotReady:
-
Token expired when trying to join:
-
kubeadm initfails with preflight errors: -
Certificate errors after cluster has been running for a year:
-
cgroup driver mismatch:
Next Steps¶
- Set up a highly available (HA) control-plane with multiple control-plane nodes and an external load balancer - see HA topology
- Add etcd encryption at rest for secrets - see Encrypting Secret Data
- Configure audit logging to track API server requests
- Set up RBAC (see Lab 31 - RBAC) for fine-grained access control
- Explore kubeadm phases (
kubeadm init phase) for granular control over cluster bootstrapping - Consider managed alternatives for production: EKS, GKE, or AKS if cluster management overhead is too high