Kubeadm Cluster Setup

Installing a production-grade multi-node Kubernetes cluster from scratch using kubeadm — the official cluster bootstrapping tool. This is the standard method for self-managed clusters on VMs, bare metal, or private cloud, and a core topic in the CKA exam (~25% weight). Source: CKA Day 27

Installation Options Landscape

Kubernetes can be installed in many ways depending on your environment and control requirements:

CategoryToolsBest For
Local / LearningKind, Minikube, K3sPOCs, CI/CD, CKA practice without cloud cost
Managed CloudEKS (AWS), AKS (Azure), GKE (GCP)Production where the cloud provider manages the control plane
Self-Managed VMskubeadm + cloud VMs (EC2, Azure VM, GCP CE)Full control, on-premise, cost optimization, CKA preparation
Self-Managed Bare Metalkubeadm + physical serversData centers, edge deployments, air-gapped environments

When to choose kubeadm: You need full control over control plane components, upgrades, etcd, and certificates. The CKA exam assumes this path.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    CONTROL PLANE NODE                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │ kube-apiserver│  │kube-scheduler│  │kube-controller-│         │
│  │   :6443      │  │   :10259     │  │   manager     │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                    etcd (:2379-2380)                │   │
│  └──────────────────────────────────────────────────────┘   │
│         ▲                                                    │
│    kubeadm init                                              │
└─────────────────────────────────────────────────────────────┘
         │
    join token (6443)
         │
┌────────┴────────────────────────────────────────────────────┐
│              WORKER NODE 1          │      WORKER NODE 2     │
│  ┌─────────────┐  ┌─────────────┐  │  ┌─────────────┐      │
│  │   kubelet   │  │  kube-proxy  │  │  │   kubelet   │      │
│  │  :10250     │  │  :10256      │  │  │  :10250     │      │
│  └─────────────┘  └─────────────┘  │  └─────────────┘      │
│         ▲                           │         ▲              │
│    kubeadm join                     │    kubeadm join        │
└─────────────────────────────────────────────────────────────┘

Prerequisites

Hardware Requirements (Minimum)

Node TypeRAMvCPUStorage
Control plane2 GB220 GB
Worker1 GB120 GB

For production, double or triple these values.

Network Requirements

All nodes must have:

  • Full ** Layer 2 or Layer 3 connectivity** between each other
  • Unique hostname, MAC address, and product_uuid per node
  • Disabled swap (kubeadm refuses to proceed if swap is active)
  • Internet access (for downloading container images) OR pre-loaded images in air-gapped environments

Required Ports

Control Plane Node:

PortProtocolComponentNotes
6443TCPkube-apiserverMust be reachable from all worker nodes and admin clients
2379–2380TCPetcd client & peerInternal to control plane only; never expose publicly
10248–10260TCPControl plane componentsScheduler (10259), controller-manager (10257), kubelet (10248/10250)
22TCPSSHAdmin access only; restrict by IP

Worker Nodes:

PortProtocolComponentNotes
10250TCPkubelet APIReachable from control plane
10256TCPkube-proxyHealth/metrics endpoint
30000–32767TCPNodePort servicesExposes Pods externally via Services
22TCPSSHAdmin access only

CKA Exam Trap: The official Kubernetes docs list control plane and worker ports separately. Remember that etcd (2379–2380) is control-plane-only and NodePort range (30000–32767) is worker-only.

Common Steps on ALL Nodes

These steps run identically on the control plane and every worker node.

1. Disable Swap

Kubernetes requires swap to be off. kubeadm will fail preflight checks otherwise.

sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab

2. Load Kernel Modules & Set Sysctl Parameters

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
  • overlay — required for containerd overlay filesystem driver
  • br_netfilter — enables iptables filtering on Linux bridges
  • net.ipv4.ip_forward — allows IP forwarding for Pod networking

3. Install containerd (Container Runtime)

Since Kubernetes 1.24, Docker shim has been removed. containerd is the default and recommended runtime.

# Download containerd binary
wget https://github.com/containerd/containerd/releases/download/v1.7.4/containerd-1.7.4-linux-amd64.tar.gz
sudo tar Cxzvf /usr/local containerd-1.7.4-linux-amd64.tar.gz
 
# Install systemd service
sudo wget -O /etc/systemd/system/containerd.service \
  https://raw.githubusercontent.com/containerd/containerd/main/containerd.service
 
# Generate default config and enable systemd cgroup driver
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
 
# Start service
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
sudo systemctl status containerd

4. Install runc

wget https://github.com/opencontainers/runc/releases/download/v1.1.9/runc.amd64
sudo install -m 755 runc.amd64 /usr/local/sbin/runc

5. Install CNI Plugins

wget https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.3.0.tgz

6. Install kubeadm, kubelet, and kubectl

# Add Kubernetes apt repository
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
 
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/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.30/deb/ /' | \
  sudo tee /etc/apt/sources.list.d/kubernetes.list
 
# Install and pin versions
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

Version alignment: Keep kubeadm, kubelet, and kubectl at the same minor version as the cluster. Mixing 1.30 control plane with 1.28 kubelet can cause compatibility issues.

sudo crictl config runtime-endpoint unix:///var/run/containerd/containerd.sock
sudo chmod -R 775 /var/run/containerd

Control Plane Initialization

kubeadm init

sudo kubeadm init \
  --pod-network-cidr=192.168.0.0/16 \
  --apiserver-advertise-address=<CONTROL_PLANE_PRIVATE_IP> \
  --node-name=master

What happens during kubeadm init:

  1. Preflight checks (swap disabled, ports free, container runtime healthy)
  2. Generates a self-signed CA and all cluster certificates (stored in /etc/kubernetes/pki)
  3. Writes Static Pod manifests to /etc/kubernetes/manifests for:
    • kube-apiserver
    • kube-controller-manager
    • kube-scheduler
    • etcd
  4. The kubelet (already running as a systemd service) reads these manifests and starts the containers
  5. Configures RBAC defaults, bootstrap tokens, and kubelet authentication
  6. Outputs a kubeadm join command with a token and CA cert hash

Configure kubectl

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

This copies the admin kubeconfig into the default kubectl search path. See Kubernetes Kubeconfig for context switching patterns.

Install CNI (Calico)

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml

Wait for all pods:

kubectl get pods -n calico-system -w
kubectl get nodes

Pod CIDR Alignment Trap: Calico’s default IP pool is 192.168.0.0/16. If you pass --pod-network-cidr=10.244.0.0/16 to kubeadm init, Calico will fail to reconcile and CoreDNS will hang in ContainerCreating. The fix is kubeadm reset and re-initializing with the correct CIDR.

Join Worker Nodes

On each worker, after completing the Common Steps, run the join command printed by kubeadm init:

sudo kubeadm join <CONTROL_PLANE_IP>:6443 \
  --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

Regenerating a Lost Join Command

# On control plane
kubeadm token create --print-join-command

Verify Cluster

kubectl get nodes
# NAME       STATUS   ROLES           AGE   VERSION
# master     Ready    control-plane   10m   v1.30.2
# worker-1   Ready    <none>          3m    v1.30.2
# worker-2   Ready    <none>          1m    v1.30.2

kubeadm Commands Reference

CommandPurpose
kubeadm initBootstraps a control plane node
kubeadm joinAdds a worker node to an existing cluster
kubeadm resetTears down kubeadm state on a node (containers, manifests, certificates)
kubeadm token create --print-join-commandGenerates a fresh join token and command
kubeadm certs check-expirationLists certificate expiry dates
kubeadm certs renew allRenews all cluster certificates
kubeadm upgrade planShows available upgrade paths
kubeadm upgrade apply v1.31.0Performs a cluster upgrade

Troubleshooting Matrix

SymptomRoot CauseFix
kubeadm init fails preflightSwap is enabledsudo swapoff -a and comment out /etc/fstab swap line
Node stays NotReadyCNI not installed or misconfiguredInstall matching CNI; check kubectl get pods -n kube-system
CoreDNS ContainerCreating indefinitelyCalico IP pool mismatch with --pod-network-cidrkubeadm reset → re-run kubeadm init with correct CIDR
kubeadm join hangs / times outSecurity group blocks 6443 or worker → master routing brokenVerify TCP 6443 reachable from worker; check VPC routing tables
crictl ps permission deniedcontainerd socket lacks permissionssudo chmod -R 775 /var/run/containerd
kubectl fails on worker nodeNo kubeconfig presentCopy admin.conf from control plane to ~/.kube/config
Control plane pods crashloopCertificate expiredkubeadm certs renew all and restart static pods

Production Hardening Checklist

  • Restrict etcd ports to internal VPC only (never public)
  • Restrict SSH (22) to bastion/jump host IP ranges
  • Use specific versions for kubeadm/kubelet/kubectl; avoid “latest”
  • Pin packages with apt-mark hold to prevent accidental upgrades
  • Back up certificates in /etc/kubernetes/pki before any change
  • Enable audit logging on the API server
  • Set up HA control plane (3+ control plane nodes + stacked or external etcd) for production
  • Regular certificate monitoring — kubeadm certs expire after 1 year by default

CKA Exam Patterns

  • Bootstrap a cluster: Given 3 VMs, install containerd, kubeadm, kubelet, run kubeadm init, install CNI, join workers
  • Port knowledge: Know which ports are control plane vs worker vs internal-only
  • Troubleshoot join failures: Check firewall, token validity, CA hash correctness
  • Certificate management: Renew expired certs with kubeadm certs renew all
  • Upgrade a cluster: kubeadm upgrade plan → drain node → upgrade packages → kubeadm upgrade apply
  • Know the default runtime: containerd (not Docker) since Kubernetes 1.24

See Also


Tags: kubernetes kubeadm cluster-installation cka production containerd calico cni devops certificates