Skip to content

Module 2 — Cluster Installation with kubeadm

Overview

kubeadm is the official Kubernetes tool for bootstrapping clusters. The CKA exam expects you to understand the full installation process: prerequisites, kubeadm init, kubeadm join, installation phases, CNI plugin deployment, and high-availability topologies.


1. Prerequisites

Before running kubeadm, every node (control plane and workers) must meet these requirements:

1.1 System Requirements

Requirement Control Plane Worker
CPU 2+ cores 1+ core
RAM 2 GB+ 1 GB+
Disk 10 GB+ 10 GB+
OS Ubuntu 22.04 / RHEL 8+ / Debian 12 Same
Swap Disabled Disabled
Unique hostname Yes Yes
Unique MAC / product_uuid Yes Yes

1.2 Disable Swap

1
2
3
4
5
# Disable swap immediately
swapoff -a

# Disable swap permanently (comment out swap entries)
sed -i '/ swap / s/^/#/' /etc/fstab

1.3 Load Required Kernel Modules

1
2
3
4
5
6
7
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

1.4 Set Sysctl Parameters

1
2
3
4
5
6
7
cat <<EOF | 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

sysctl --system

1.5 Install a Container Runtime (containerd)

# Install containerd
apt-get update && apt-get install -y containerd

# Generate default config
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml

# Enable SystemdCgroup (required for kubeadm)
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# Restart containerd
systemctl restart containerd
systemctl enable containerd

Why SystemdCgroup? kubelet uses systemd as the cgroup driver by default. The container runtime must match. Mismatched cgroup drivers cause kubelet to fail.

1.6 Install kubeadm, kubelet, and kubectl

# Add Kubernetes apt repository (v1.30 example)
apt-get update && apt-get install -y apt-transport-https ca-certificates curl gpg

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | \
  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/ /' | \
  tee /etc/apt/sources.list.d/kubernetes.list

# Install
apt-get update
apt-get install -y kubelet kubeadm kubectl

# Pin versions to prevent accidental upgrades
apt-mark hold kubelet kubeadm kubectl

1.7 Required Ports

Component Ports Protocol
kube-apiserver 6443 TCP
etcd 2379-2380 TCP
kubelet 10250 TCP
kube-scheduler 10259 TCP
kube-controller-manager 10257 TCP
NodePort Services 30000-32767 TCP
# Verify ports are open (on each node)
nc -zv <control-plane-ip> 6443

2. Initializing the Control Plane — kubeadm init

2.1 Basic Init

1
2
3
4
kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --apiserver-advertise-address=192.168.1.10 \
  --kubernetes-version=v1.30.0
Flag Purpose
--pod-network-cidr CIDR for the pod network — must match your CNI plugin's expected range
--apiserver-advertise-address IP the API server advertises to other components
--kubernetes-version Specific K8s version to install
--control-plane-endpoint Stable endpoint for HA setups (DNS name or load balancer IP)
--service-cidr CIDR for Services (default: 10.96.0.0/12)
--cri-socket Container runtime socket (auto-detected if only one runtime is installed)

2.2 What kubeadm init Does (Phases)

kubeadm init runs through a series of phases in order:

kubeadm init
  ├── preflight          ← Validates system requirements
  ├── certs              ← Generates all PKI certificates
  │   ├── ca
  │   ├── apiserver
  │   ├── apiserver-kubelet-client
  │   ├── front-proxy-ca
  │   ├── front-proxy-client
  │   ├── etcd/ca
  │   ├── etcd/server
  │   ├── etcd/peer
  │   ├── etcd/healthcheck-client
  │   ├── apiserver-etcd-client
  │   └── sa (service account key pair)
  ├── kubeconfig         ← Generates kubeconfig files
  │   ├── admin.conf
  │   ├── kubelet.conf
  │   ├── controller-manager.conf
  │   └── scheduler.conf
  ├── kubelet-start       ← Writes kubelet config and starts kubelet
  ├── control-plane       ← Creates static pod manifests
  │   ├── apiserver
  │   ├── controller-manager
  │   └── scheduler
  ├── etcd               ← Creates etcd static pod manifest
  ├── upload-config      ← Uploads kubeadm and kubelet config to ConfigMaps
  ├── upload-certs       ← (HA only) Uploads certs to a Secret
  ├── mark-control-plane ← Adds labels and taints to the control plane node
  ├── bootstrap-token    ← Creates bootstrap token for joining nodes
  └── addon              ← Installs CoreDNS and kube-proxy

CKA Tip: You can run individual phases with kubeadm init phase <phase-name>. This is useful for troubleshooting or custom setups.

1
2
3
4
5
6
7
8
# Example: regenerate certificates only
kubeadm init phase certs all

# Example: generate only the API server certificate
kubeadm init phase certs apiserver

# List all available phases
kubeadm init phase --help

2.3 After kubeadm init

The output gives you three critical pieces of information:

# 1. Set up kubectl for the admin user
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

# 2. Install a CNI plugin (covered in Section 4)
kubectl apply -f <cni-manifest-url>

# 3. Join command for worker nodes (save this!)
kubeadm join 192.168.1.10:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc123...

2.4 Using a Configuration File

Instead of passing flags, you can use a YAML configuration file:

# kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.30.0
controlPlaneEndpoint: "k8s-lb.example.com:6443"
networking:
  podSubnet: "10.244.0.0/16"
  serviceSubnet: "10.96.0.0/12"
  dnsDomain: "cluster.local"
apiServer:
  certSANs:
    - "k8s-lb.example.com"
    - "192.168.1.100"
etcd:
  local:
    dataDir: /var/lib/etcd
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "192.168.1.10"
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///var/run/containerd/containerd.sock
kubeadm init --config kubeadm-config.yaml

CKA Tip: You can print the default configuration with:

kubeadm config print init-defaults
kubeadm config print join-defaults


3. Joining Worker Nodes — kubeadm join

3.1 Using the Join Command

1
2
3
4
# On each worker node (use the command from kubeadm init output)
kubeadm join 192.168.1.10:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc123...

3.2 What kubeadm join Does

1
2
3
4
5
6
kubeadm join
  ├── preflight          ← Validates system requirements
  ├── discovery          ← Contacts API server, validates CA cert hash
  ├── kubelet-start      ← Configures and starts kubelet
  └── (optional) control-plane  ← If --control-plane flag is used

3.3 Token Management

Tokens expire after 24 hours by default.

# List existing tokens
kubeadm token list

# Create a new token
kubeadm token create

# Create a new token and print the full join command
kubeadm token create --print-join-command

# Get the CA cert hash (if you lost it)
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
  openssl rsa -pubin -outform der 2>/dev/null | \
  openssl dgst -sha256 -hex | sed 's/^.* //'

CKA Tip: kubeadm token create --print-join-command is the fastest way to get a working join command. Memorize this.

3.4 Verify Nodes Joined

1
2
3
4
5
kubectl get nodes
# NAME           STATUS     ROLES           AGE   VERSION
# controlplane   NotReady   control-plane   5m    v1.30.0
# worker-1       NotReady   <none>          2m    v1.30.0
# worker-2       NotReady   <none>          1m    v1.30.0

Nodes will show NotReady until a CNI plugin is installed.


4. Choosing and Installing a CNI Plugin

4.1 Why CNI Is Required

Kubernetes does not ship with a built-in network implementation. The Container Network Interface (CNI) plugin provides:

  • Pod-to-pod networking across nodes
  • IP address management (IPAM) for pods
  • Network policy enforcement (plugin-dependent)

Without a CNI plugin: - Nodes remain NotReady - CoreDNS pods stay in Pending state - No pod-to-pod communication is possible

4.2 CNI Plugin Comparison

Feature Calico Flannel Cilium
Network Policy ✅ Full support ❌ None ✅ Full + extended
Encryption WireGuard None WireGuard / IPsec
Routing BGP, VXLAN, IP-in-IP VXLAN VXLAN, native routing
Performance High Moderate High (eBPF)
Complexity Medium Low High
CKA relevance Most commonly tested Simple labs Growing adoption

CKA Tip: The exam typically uses Calico or Flannel. The install command is usually provided in the exam question or you can find it in the official docs.

4.3 Installing Calico

# Calico with default settings (pod CIDR: 192.168.0.0/16)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml

If you used --pod-network-cidr=10.244.0.0/16 during init, modify the Calico manifest:

1
2
3
4
5
6
7
8
9
# Download the manifest
curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml

# Edit the CALICO_IPV4POOL_CIDR to match your pod CIDR
# Find and uncomment/modify:
#   - name: CALICO_IPV4POOL_CIDR
#     value: "10.244.0.0/16"

kubectl apply -f calico.yaml

4.4 Installing Flannel

# Flannel expects --pod-network-cidr=10.244.0.0/16
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Important: Flannel's default CIDR is 10.244.0.0/16. Your --pod-network-cidr must match.

4.5 Installing Cilium

1
2
3
4
5
6
# Using Helm (preferred)
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system

# Or using the Cilium CLI
cilium install

4.6 Verifying CNI Installation

# Nodes should become Ready
kubectl get nodes
# NAME           STATUS   ROLES           AGE   VERSION
# controlplane   Ready    control-plane   10m   v1.30.0
# worker-1       Ready    <none>          7m    v1.30.0

# CoreDNS pods should be Running
kubectl get pods -n kube-system | grep coredns

# Check CNI configuration on a node
ls /etc/cni/net.d/
cat /etc/cni/net.d/*.conflist

# Check CNI binaries
ls /opt/cni/bin/

# Test pod-to-pod connectivity
kubectl run test1 --image=busybox --command -- sleep 3600
kubectl run test2 --image=busybox --command -- sleep 3600
kubectl exec test1 -- ping -c 3 $(kubectl get pod test2 -o jsonpath='{.status.podIP}')

4.7 CNI Troubleshooting

Symptom Likely Cause Fix
Nodes stuck in NotReady CNI not installed Install a CNI plugin
CoreDNS pods Pending No CNI available Install a CNI plugin
Pods can't communicate across nodes CIDR mismatch Ensure --pod-network-cidr matches CNI config
CNI pods CrashLoopBackOff Missing kernel modules Load overlay and br_netfilter modules
Pods get IP but can't reach other pods Firewall blocking VXLAN (UDP 4789) or IP-in-IP (protocol 4) Open required ports between nodes

5. kubeadm init Phases — Deep Dive

Understanding phases is critical for troubleshooting and for the exam.

5.1 Preflight Checks

1
2
3
4
5
6
7
8
# Run preflight checks without actually initializing
kubeadm init phase preflight

# Common preflight failures:
# - [ERROR Swap]: swap is enabled
# - [ERROR Port-6443]: port 6443 is in use
# - [ERROR FileAvailable--etc-kubernetes-manifests-*]: manifest already exists
# - [ERROR NumCPU]: the number of CPUs is less than required

5.2 Certificate Generation

kubeadm generates a full PKI under /etc/kubernetes/pki/:

/etc/kubernetes/pki/
├── ca.crt                    # Cluster CA certificate
├── ca.key                    # Cluster CA private key
├── apiserver.crt             # API server serving certificate
├── apiserver.key
├── apiserver-kubelet-client.crt
├── apiserver-kubelet-client.key
├── apiserver-etcd-client.crt
├── apiserver-etcd-client.key
├── front-proxy-ca.crt        # Front proxy CA
├── front-proxy-ca.key
├── front-proxy-client.crt
├── front-proxy-client.key
├── sa.key                    # Service account signing key
├── sa.pub                    # Service account verification key
└── etcd/
    ├── ca.crt                # etcd CA certificate
    ├── ca.key
    ├── server.crt
    ├── server.key
    ├── peer.crt
    ├── peer.key
    ├── healthcheck-client.crt
    └── healthcheck-client.key
1
2
3
4
5
6
7
8
# Check certificate expiration
kubeadm certs check-expiration

# Renew all certificates
kubeadm certs renew all

# Renew a specific certificate
kubeadm certs renew apiserver

CKA Tip: Certificates are valid for 1 year by default. The CA certificate is valid for 10 years.

5.3 Static Pod Manifests

After init, control plane components run as static pods:

1
2
3
4
5
ls /etc/kubernetes/manifests/
# etcd.yaml
# kube-apiserver.yaml
# kube-controller-manager.yaml
# kube-scheduler.yaml

kubelet watches this directory and automatically creates/restarts these pods. If you modify a manifest, kubelet detects the change and recreates the pod.


6. High-Availability Control Plane

6.1 Why HA?

A single control plane node is a single point of failure. If it goes down: - No new pods can be scheduled - No scaling or self-healing - No API access (kubectl stops working) - Existing workloads continue running on worker nodes (kubelet keeps them alive)

6.2 HA Topologies

kubeadm supports two HA topologies:

etcd runs on the same nodes as the control plane components.

          +----------------------+
          |    Load Balancer     |
          |  k8s-lb:6443         |
          +----------+-----------+
                     |
     +---------------+---------------+
     |               |               |
+----+------+   +----+------+   +----+------+
| CP Node1  |   | CP Node2  |   | CP Node3  |
|           |   |           |   |           |
| apiserver |   | apiserver |   | apiserver |
| scheduler |   | scheduler |   | scheduler |
| ctrl-mgr  |   | ctrl-mgr  |   | ctrl-mgr  |
| etcd      |   | etcd      |   | etcd      |
+-----------+   +-----------+   +-----------+
  • Pros: Fewer nodes, simpler setup
  • Cons: Losing a node loses both a control plane member and an etcd member

External etcd

etcd runs on dedicated nodes, separate from the control plane.

        ┌──────────────────────┐
        │    Load Balancer     │
        │  k8s-lb:6443         │
        └──────────┬───────────┘
     ┌─────────────┼─────────────┐
     │             │             │
┌────▼────┐  ┌────▼────┐  ┌────▼────┐
│ CP Node1│  │ CP Node2│  │ CP Node3│
│         │  │         │  │         │
│ apiserver│  │ apiserver│  │ apiserver│
│ scheduler│  │ scheduler│  │ scheduler│
│ ctrl-mgr │  │ ctrl-mgr │  │ ctrl-mgr │
└─────────┘  └─────────┘  └─────────┘

┌─────────┐  ┌─────────┐  ┌─────────┐
│ etcd-1  │  │ etcd-2  │  │ etcd-3  │
└─────────┘  └─────────┘  └─────────┘
  • Pros: etcd failure doesn't affect control plane, independent scaling
  • Cons: More nodes to manage, more complex setup

6.3 Load Balancer Requirement

HA requires a load balancer in front of the API servers:

Option Type Notes
HAProxy + keepalived On-premise Most common for bare-metal
Cloud LB (NLB/ALB) Cloud AWS, Azure, GCP native LBs
kube-vip Kubernetes-native Virtual IP, runs as static pod

Example HAProxy configuration:

frontend k8s-api
    bind *:6443
    mode tcp
    default_backend k8s-api-backend

backend k8s-api-backend
    mode tcp
    balance roundrobin
    option tcp-check
    server cp1 192.168.1.10:6443 check
    server cp2 192.168.1.11:6443 check
    server cp3 192.168.1.12:6443 check

6.4 Setting Up HA with kubeadm

Step 1 — Initialize the first control plane node

1
2
3
4
kubeadm init \
  --control-plane-endpoint "k8s-lb.example.com:6443" \
  --upload-certs \
  --pod-network-cidr=10.244.0.0/16
  • --control-plane-endpoint — points to the load balancer (critical for HA)
  • --upload-certs — encrypts and uploads certificates to a kubeadm-certs Secret

Step 2 — Join additional control plane nodes

1
2
3
4
5
6
# The init output provides this command:
kubeadm join k8s-lb.example.com:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc123... \
  --control-plane \
  --certificate-key <certificate-key>
  • --control-plane — tells kubeadm this node is a control plane member
  • --certificate-key — decryption key for the uploaded certificates (valid for 2 hours)

Step 3 — Join worker nodes

1
2
3
kubeadm join k8s-lb.example.com:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc123...

6.5 etcd Quorum

etcd uses the Raft consensus algorithm and requires a majority (quorum) to function:

etcd Members Quorum Tolerated Failures
1 1 0
2 2 0 (worse than 1!)
3 2 1
5 3 2
7 4 3

Key insight: Always use an odd number of etcd members. 2 members is worse than 1 because you still need both for quorum but have more points of failure.

6.6 Leader Election

In an HA setup, only one instance of the scheduler and controller manager is active at a time. The others are on standby using leader election:

1
2
3
4
5
# Check which node holds the scheduler lease
kubectl get lease kube-scheduler -n kube-system -o yaml

# Check which node holds the controller-manager lease
kubectl get lease kube-controller-manager -n kube-system -o yaml

The API server, however, runs active-active — all instances serve requests simultaneously behind the load balancer.


7. Resetting a Cluster

If you need to start over:

# On each node (control plane and workers)
kubeadm reset

# Clean up CNI configuration
rm -rf /etc/cni/net.d

# Clean up iptables rules
iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X

# (Optional) Clean up IPVS rules
ipvsadm --clear

# Remove kubeconfig
rm -rf $HOME/.kube/config

8. Practice Exercises

Exercise 1 — Install a Cluster from Scratch

# On ALL nodes:
# 1. Disable swap
# 2. Load kernel modules (overlay, br_netfilter)
# 3. Set sysctl parameters
# 4. Install containerd with SystemdCgroup = true
# 5. Install kubeadm, kubelet, kubectl

# On the control plane node:
# 6. Run kubeadm init with --pod-network-cidr=10.244.0.0/16
# 7. Set up kubectl
# 8. Install Calico or Flannel
# 9. Verify nodes are Ready

# On each worker node:
# 10. Run kubeadm join with the token from step 6
# 11. Verify all nodes are Ready from the control plane

Exercise 2 — Explore kubeadm Phases

# 1. Print the default init configuration
kubeadm config print init-defaults

# 2. Run only the preflight checks
kubeadm init phase preflight --config kubeadm-config.yaml

# 3. Check certificate expiration dates
kubeadm certs check-expiration

# 4. List all certificates in /etc/kubernetes/pki/
find /etc/kubernetes/pki/ -name "*.crt" -exec openssl x509 -in {} -noout -subject -dates \;

Exercise 3 — Token Management

# 1. List all bootstrap tokens
kubeadm token list

# 2. Create a new token with a 1-hour TTL
kubeadm token create --ttl 1h

# 3. Generate a full join command
kubeadm token create --print-join-command

# 4. Manually compute the CA cert hash
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
  openssl rsa -pubin -outform der 2>/dev/null | \
  openssl dgst -sha256 -hex | sed 's/^.* //'

Exercise 4 — Break and Fix

# Scenario 1: Wrong pod CIDR
# - Initialize a cluster with --pod-network-cidr=10.244.0.0/16
# - Install Calico with default CIDR (192.168.0.0/16)
# - Observe: pods get IPs from 192.168.x.x, not 10.244.x.x
# - Fix: modify the Calico manifest to use 10.244.0.0/16

# Scenario 2: Missing CNI
# - After kubeadm init, don't install a CNI
# - Observe: nodes are NotReady, CoreDNS is Pending
# - Fix: install a CNI plugin

# Scenario 3: Expired token
# - Try to join a worker node with an expired token
# - Observe: "token has expired" error
# - Fix: kubeadm token create --print-join-command

9. Key Takeaways for the CKA Exam

Point Detail
Know the prerequisites Swap off, kernel modules, sysctl params, container runtime, kubeadm/kubelet/kubectl
kubeadm init phases Understand what each phase does — you may need to run individual phases
--pod-network-cidr must match CNI Flannel expects 10.244.0.0/16, Calico defaults to 192.168.0.0/16
Tokens expire in 24h Use kubeadm token create --print-join-command to regenerate
HA requires a load balancer --control-plane-endpoint must point to the LB, not a single node
Stacked vs external etcd Know both topologies and their trade-offs
Odd number of etcd members 3 or 5 — never 2
Leader election Scheduler and controller manager use leases; API server is active-active
kubeadm reset Cleans up a node — remember to also clean CNI config and iptables
Config file approach kubeadm init --config is cleaner than many CLI flags

Next: 03-cluster-upgrade.md — Upgrading a cluster with kubeadm