Skip to content

Preparing Infrastructure and Installing Clusters with kubeadm

Step 1 of 24

CKA kubeadm Installation: Prepare Nodes and Bootstrap a Cluster

Introduction

This lesson belongs to the Cluster Architecture, Installation & Configuration domain of the CKA curriculum (check the exam environment version below), which carries 25% of the exam. Together with troubleshooting, it is where most of the practical, command-line work of the exam is concentrated. The CKA is a performance-based exam: you are given a terminal, one or more live clusters, and a list of tasks. The published curriculum includes creating and managing clusters with kubeadm. Do not assume a particular task frequency; you need to understand how a kubeadm cluster is built, because every later task (upgrading, backing up etcd, repairing a broken control plane) depends on that understanding.

By the end of this lesson you will be able to prepare a Linux node for Kubernetes, bootstrap a control plane with kubeadm init, join worker nodes, explain what each control plane component does and where its manifest lives, and work confidently with kubeconfig files and contexts.

Version check (October 26, 2026). The Linux Foundation FAQ currently lists Kubernetes v1.37 for CKA. Match your lab packages and documentation to the target minor version, and recheck the FAQ before the exam. The YAML below illustrates v1.37.0; use a supported patch release available for your lab.

Learning objectives

  • List the prerequisites a node must meet before kubeadm is run: container runtime, swap, kernel modules, sysctl settings, open ports, unique identity.
  • Run kubeadm init with the options that matter (--pod-network-cidr, --control-plane-endpoint, --apiserver-advertise-address, a configuration file) and interpret its output.
  • Join worker and control plane nodes, regenerate expired tokens and discover the CA certificate hash.
  • Describe the role of kube-apiserver, etcd, kube-scheduler, kube-controller-manager, kubelet, kube-proxy and the container runtime, and find the static Pod manifests that run the control plane.
  • Read and edit kubeconfig files, switch contexts and namespaces, and create a kubeconfig for another user or cluster.

The big picture: what kubeadm does and does not do

kubeadm is the official tool for bootstrapping a conformant cluster on machines you already have. It is deliberately narrow in scope. It does:

  • generate the cluster certificate authority (CA) and component certificates under /etc/kubernetes/pki;
  • write kubeconfig files for the admin, the kubelet, the controller manager and the scheduler under /etc/kubernetes;
  • write static Pod manifests for kube-apiserver, kube-controller-manager, kube-scheduler and (for a stacked topology) etcd under /etc/kubernetes/manifests;
  • start the kubelet, wait for the control plane Pods to become healthy, and install two add-ons: CoreDNS and kube-proxy;
  • create a bootstrap token so other nodes can join.

It does not: install a container runtime, install the kubelet and kubeadm packages, provision machines, or install a Pod network add-on (CNI plugin). Until a CNI plugin is installed, nodes stay NotReady and CoreDNS Pods stay Pending. This is the single most common observation after a first kubeadm init, and a common exam trap.

Responsibility Done by kubeadm? Who does it
Install OS packages (kubelet, kubeadm, kubectl) No You, with the distribution package manager
Install and configure the container runtime No You (containerd, CRI-O)
Create certificates and kubeconfig files Yes kubeadm init
Create control plane static Pods Yes kubeadm init
Install CoreDNS and kube-proxy Yes kubeadm init
Install a CNI plugin No You (Calico, Cilium, Flannel, ...)
Join additional nodes Partly kubeadm join run on each node

Preparing the nodes

Every node (control plane and worker) must be prepared in the same way before kubeadm is used.

Hardware, OS and identity

The Kubernetes documentation asks for a compatible Linux host with at least 2 GB of RAM per machine and 2 CPUs for control plane machines, full network connectivity between all machines, and a unique hostname, MAC address and product_uuid on each node. Duplicate identities lead to confusing, intermittent join problems. You can check the values with:

Bash
hostnamectl --static
ip link
sudo cat /sys/class/dmi/id/product_uuid

Container runtime

Kubernetes talks to the runtime through the Container Runtime Interface (CRI). Since Kubernetes 1.24 the built-in Docker integration (dockershim) is gone, so the runtime must be a CRI-compatible one, most commonly containerd or CRI-O. The runtime exposes a socket; kubeadm detects it automatically when exactly one is present:

Runtime Default socket
containerd unix:///var/run/containerd/containerd.sock
CRI-O unix:///var/run/crio/crio.sock

Two settings matter. First, the cgroup driver: on systemd-based distributions the kubelet and the runtime must both use the systemd cgroup driver. In containerd's /etc/containerd/config.toml that means SystemdCgroup = true in the runc options. A mismatch makes the kubelet or Pods restart in loops. Recent kubeadm versions default the kubelet to the systemd driver, so the runtime configuration is the part you usually fix. Second, if more than one runtime socket exists, pass --cri-socket to kubeadm init and kubeadm join.

Swap

By default the kubelet refuses to start when swap is enabled. The exam-safe preparation is:

Bash
sudo swapoff -a
# make it permanent: comment the swap line in /etc/fstab
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Swap support exists in recent Kubernetes versions (the NodeSwap feature, with the kubelet setting failSwapOn: false and a swapBehavior), but it is an opt-in, advanced configuration. For the CKA, assume swap must be off unless a task explicitly says otherwise.

Kernel modules and sysctl settings

For a typical containerd lab using bridge/iptables networking, prepare the following modules and forwarding settings. Exact requirements depend on the container runtime and CNI implementation:

Bash
cat <<EOT | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOT
sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOT | 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
EOT
sudo sysctl --system

overlay supports the overlay filesystem used by container image layers; br_netfilter makes bridged traffic visible to iptables for network implementations that require it; ip_forward lets the node route packets between Pods and the outside. A kubeadm preflight failure such as /proc/sys/net/ipv4/ip_forward contents are not set to 1 is solved by exactly this step.

Ports

Make sure the firewall and cloud security groups allow the required ports. These come straight from the official documentation and are worth memorising:

Node Port Protocol Used by
Control plane 6443 TCP Kubernetes API server (everyone)
Control plane 2379-2380 TCP etcd server client API and peer traffic (kube-apiserver, etcd)
Control plane 10250 TCP kubelet API (control plane, itself)
Control plane 10259 TCP kube-scheduler (itself)
Control plane 10257 TCP kube-controller-manager (itself)
Worker 10250 TCP kubelet API
Worker 10256 TCP kube-proxy health check
Worker 30000-32767 TCP/UDP NodePort Services (default range)

Installing the packages

Install kubeadm, kubelet and kubectl from the Kubernetes package repository for your minor version (pkgs.k8s.io), after configuring the repository signing key and source as shown in the official installation instructions. Then hold the packages so that an unattended upgrade does not move the cluster to a different version:

Bash
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubelet

After this, the kubelet restarts every few seconds in a crash loop. That is normal: it is waiting for kubeadm init or kubeadm join to give it configuration.

Worked example: diagnosing a failed preflight

Scenario. Contoso is building a three-node lab cluster on Ubuntu VMs. On the first VM, sudo kubeadm init stops with:

Text
[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
  [ERROR Swap]: running with swap on is not supported. Please disable swap
  [ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1

Reasoning, step by step.

  1. Read the error names. Swap and FileContent--proc-sys-net-ipv4-ip_forward are two independent failures; kubeadm lists all fatal ones at once.
  2. Fix swap: sudo swapoff -a, then comment the entry in /etc/fstab so the fix survives a reboot.
  3. Fix forwarding: create /etc/sysctl.d/k8s.conf with net.ipv4.ip_forward = 1 and run sudo sysctl --system.
  4. Re-run kubeadm init. Do not use --ignore-preflight-errors=all to silence the problem: it hides real misconfigurations and the node would fail later in a harder-to-diagnose way.

Why the alternatives are worse. Passing --ignore-preflight-errors=Swap would let the init proceed, but the kubelet would still refuse to run with swap on, so the control plane Pods would never start. Rebooting without editing /etc/fstab or the sysctl file would reintroduce both errors.

Bootstrapping the control plane with kubeadm init

The simplest form

On the first control plane node:

Bash
sudo kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --apiserver-advertise-address=192.168.10.11
  • --pod-network-cidr reserves the range from which Pods get IP addresses. Some CNI plugins need a specific range; others read this value or have their own default. It must not overlap with node or Service networks.
  • --apiserver-advertise-address is the IP the API server advertises. If the machine has several interfaces, set it explicitly.
  • --service-cidr (default 10.96.0.0/12) is the virtual IP range for Services.
  • --kubernetes-version pins the control plane version (defaults to the latest stable matching the kubeadm binary).
  • --control-plane-endpoint sets a stable DNS name or load balancer address for the API server. Set it from day one if you may ever add control plane nodes; it cannot easily be added later and is required for high availability (covered in the next lesson).
  • --upload-certs uploads control plane certificates (encrypted with a key kubeadm prints) so that additional control plane nodes can fetch them when they join.

A configuration file is better for repeatability

Flags and a file cannot be mixed freely; for anything non-trivial use a ClusterConfiguration and InitConfiguration:

YAML
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.10.11
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///var/run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.37.0
controlPlaneEndpoint: k8s-api.contoso.example:6443
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12

Run it with sudo kubeadm init --config kubeadm-config.yaml. If you do not remember the field names during the exam, kubeadm config print init-defaults prints a complete template, and the official documentation is open in the allowed browser tab.

What happens during init (phases)

kubeadm init runs ordered phases, and you can run them individually with kubeadm init phase <name>:

  1. preflight: checks swap, ports, runtime, CPU and memory.
  2. certs: creates the CA and certificates in /etc/kubernetes/pki (API server, kubelet client, front-proxy, etcd, service account keys).
  3. kubeconfig: writes admin.conf, kubelet.conf, controller-manager.conf, scheduler.conf.
  4. etcd and control-plane: write static Pod manifests in /etc/kubernetes/manifests.
  5. kubelet-start: writes the kubelet configuration and restarts it; the kubelet notices the manifests and starts the control plane Pods.
  6. upload-config, mark-control-plane: store configuration in ConfigMaps in kube-system and taint the node node-role.kubernetes.io/control-plane:NoSchedule, so ordinary workloads are not scheduled there.
  7. bootstrap-token and addon: create the join token and deploy CoreDNS and kube-proxy.

After init: kubeconfig and the CNI plugin

The output tells you to configure kubectl for a regular user:

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

Then install a CNI plugin by applying its manifest, for example kubectl apply -f <cni-manifest>.yaml, and watch the nodes become Ready. Confirm with:

Bash
kubectl get nodes
kubectl get pods -n kube-system

Joining nodes

At the end of kubeadm init, a ready-made command is printed:

Bash
sudo kubeadm join 192.168.10.11:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:3f4a...e91

Two values authenticate the exchange in both directions: the token proves to the control plane that the joiner is allowed in, and the CA certificate hash lets the joiner verify that it is talking to the genuine API server (preventing man-in-the-middle attacks). Tokens expire after 24 hours by default. If yours has expired:

Bash
sudo kubeadm token list
sudo kubeadm token create --print-join-command      # new token and the full command

If you only lost the hash, compute it from the CA:

Bash
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \
  | openssl rsa -pubin -outform der 2>/dev/null \
  | openssl dgst -sha256 -hex | sed 's/^.* //'

To add a control plane node, add --control-plane and --certificate-key <key> (the key printed by kubeadm init --upload-certs; the uploaded certificate Secret is deleted after two hours, so re-upload with sudo kubeadm init phase upload-certs --upload-certs if needed). To remove a node properly: kubectl drain <node> --ignore-daemonsets --delete-emptydir-data, kubectl delete node <node>, then run sudo kubeadm reset on the node itself and clean up CNI configuration under /etc/cni/net.d.

Worked example: a join that fails with an x509 error

Scenario. The platform team of Fabrikam rebuilt the control plane node and created a replacement cluster after removing the old PKI during an authorized rebuild. A worker that was prepared earlier now fails with x509: certificate signed by unknown authority when it runs the old kubeadm join command.

Reasoning.

  1. The error is about trust, not connectivity: the TCP connection to port 6443 works.
  2. The hash in the old command matches the previous CA. In this explicitly rebuilt cluster, the team generated a new CA, so the old hash and old token are both invalid.
  3. On the new control plane run kubeadm token create --print-join-command to get a matching token and hash.
  4. On the worker, wipe the stale state first: sudo kubeadm reset -f, then run the new join command.
  5. Verify with kubectl get nodes on the control plane.

Why not the alternatives. Using --discovery-token-unsafe-skip-ca-verification would make the error disappear but turns off the protection against impersonation; it should not be used as a substitute for restoring the correct trust configuration. Copying the old CA file to the new control plane is possible in advanced recovery scenarios, but is not a join fix.

The control plane components

Component Where it runs Role
kube-apiserver Static Pod on control plane The normal Kubernetes API front door to etcd. Authenticates, authorises, and applies admission to relevant requests; persists resource changes. Administrative tools can also access etcd directly.
etcd Static Pod (stacked) or external cluster Consistent key-value store holding all cluster state.
kube-scheduler Static Pod Watches for unscheduled Pods and assigns each to a node (filtering and scoring).
kube-controller-manager Static Pod Runs controllers (Deployment, ReplicaSet, Node, Job, EndpointSlice, ServiceAccount ...) that reconcile actual state toward desired state.
cloud-controller-manager Optional Integrates with a cloud provider (load balancers, node lifecycle, routes).
kubelet systemd service on every node Starts Pods on its node via the CRI, reports status, runs probes, and launches static Pods from the manifest directory.
kube-proxy DaemonSet Programs the configured Service proxy implementation (for example iptables or nftables; older clusters can use IPVS) to implement Service virtual IPs.
Container runtime systemd service on every node Pulls images and runs containers (containerd, CRI-O).

Because the control plane runs as static Pods, you manage it by editing files, not by kubectl edit. The kubelet watches the directory defined by staticPodPath in /var/lib/kubelet/config.yaml (by default /etc/kubernetes/manifests). Saving a changed manifest makes the kubelet recreate the Pod; deleting the file stops it. Mirror Pods named kube-apiserver-<node> appear in the API for visibility, but deleting them with kubectl has no lasting effect. This matters on the exam: to change an API server flag, edit /etc/kubernetes/manifests/kube-apiserver.yaml and wait; to diagnose a control plane failure, inspect those same files.

Worked example: adding an API server flag

Task. Enable file-based API auditing with an audit policy and --audit-log-path=/var/log/kubernetes/audit/audit.log. The log-path flag alone does not define which audit events to generate.

Steps.

  1. sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak (always keep a copy outside the manifests directory; a backup left inside would be started as a second Pod).
  2. Create /etc/kubernetes/audit-policy.yaml with a valid audit.k8s.io/v1 Policy and a nonempty rules list, such as rules: [{level: Metadata}]. Add --audit-policy-file=/etc/kubernetes/audit-policy.yaml and --audit-log-path=/var/log/kubernetes/audit/audit.log under the API server command. Mount the policy file read-only and the log directory writable, with matching hostPath volumes. The container must be able to read the policy and write the log.
  3. Save. The kubelet recreates the Pod; the API is unavailable for a short time, so kubectl may fail for 30-60 seconds.
  4. Check with sudo crictl ps | grep kube-apiserver. If it keeps restarting, read sudo crictl logs <id> and restore the backup. After recovery, make an API request covered by the policy and verify that the host audit log receives an event.

Why this approach. For this kubeadm static-Pod deployment, edit the source manifest rather than the mirror Pod. Also preserve intended changes in the supported kubeadm configuration workflow so later upgrades do not overwrite them.

kubeconfig files and contexts

A kubeconfig is a YAML file with three lists and one pointer:

  • clusters: the API server URL and the CA data (certificate-authority-data);
  • users: credentials (client certificate and key, token, or an exec plugin);
  • contexts: a named triple of cluster + user + namespace;
  • current-context: the context kubectl uses.

kubectl locates configuration in this order: the --kubeconfig flag, then the KUBECONFIG environment variable (a merged list separated by colons on Linux/macOS or semicolons on Windows), then ~/.kube/config.

Bash
kubectl config get-contexts
kubectl config current-context
kubectl config use-context prod-admin
kubectl config set-context --current --namespace=payments
kubectl config view --minify            # only the active context
kubectl config view --raw               # reveals embedded certificate data

On the exam, every task states the context to use (for example kubectl config use-context k8s-c2). Forgetting to switch is the most expensive mistake you can make: a perfect solution applied to the wrong cluster scores nothing. Copy the provided command at the beginning of each task.

You can build a kubeconfig by hand with kubectl config set-cluster, set-credentials and set-context:

Bash
kubectl config set-cluster staging --server=https://10.0.0.5:6443 \
  --certificate-authority=/etc/kubernetes/pki/ca.crt --embed-certs=true --kubeconfig=dev.conf
kubectl config set-credentials dev-user --client-certificate=dev.crt \
  --client-key=dev.key --embed-certs=true --kubeconfig=dev.conf
kubectl config set-context dev --cluster=staging --user=dev-user \
  --namespace=dev --kubeconfig=dev.conf

Current kubeadm generates admin.conf for kubernetes-admin in the kubeadm:cluster-admins group, bound to the powerful cluster-admin ClusterRole. The separate super-admin.conf uses the system:masters group and bypasses the authorization layer. Protect both files and reserve the latter for break-glass recovery. Give people separate identities and appropriate Kubernetes RBAC bindings.

For upgrades, etcd recovery, and additional control plane nodes, continue with CKA cluster lifecycle and high availability.

Common pitfalls and exam tips

  • Nodes NotReady right after init is expected until a CNI plugin is applied. Do not reinstall; apply the CNI.
  • Swap and kernel settings are lost on reboot if you only run swapoff -a or modprobe without persisting them.
  • cgroup driver mismatch between kubelet and runtime causes crash loops; check SystemdCgroup = true.
  • Expired token: tokens last 24 hours; regenerate with kubeadm token create --print-join-command.
  • Pod CIDR overlap with the node network silently breaks routing.
  • Backups inside /etc/kubernetes/manifests become live Pods or crash the kubelet's parsing; keep them elsewhere.
  • Wrong context. Always run kubectl config use-context as the first command of each task.
  • Exam tip: use the allowed documentation at kubernetes.io; search for "kubeadm" pages, and learn where kubeadm join, kubeadm token and the ports table live. Set alias k=kubectl and export do="--dry-run=client -o yaml" to save time, and enable shell completion.
  • Exam tip: the exam is time-boxed (120 minutes) with a 66% passing score. Skip a slow task and come back; weighted tasks are shown with their value.

Mini case study: bootstrapping for Tailwind Traders

Situation. Tailwind Traders wants a cluster in a datacentre: one control plane node cp1 (192.168.20.10) and two workers. They plan to add two more control plane nodes next quarter. The first attempt used kubeadm init --pod-network-cidr=192.168.0.0/16, but the datacentre network is 192.168.20.0/24.

Questions.

  1. What is wrong with the Pod CIDR?
  2. What must change now so that adding control plane nodes later is possible?
  3. Nodes show NotReady and CoreDNS is Pending after init. Is the init broken?

Solution, commented.

  1. The Pod range 192.168.0.0/16 contains the node subnet, so Pod and node addresses overlap and routing becomes ambiguous. Choose a range such as 10.244.0.0/16 that does not overlap with node or Service ranges.
  2. Initialise with --control-plane-endpoint=k8s-api.tailwind.example:6443 (a DNS name or load balancer that can later point at three API servers) and --upload-certs. Because the endpoint is baked into certificates and kubeconfigs, it is far easier to set it now than to retrofit it.
  3. No. kubeadm installs CoreDNS but not networking. Apply the chosen CNI plugin and the nodes become Ready, then CoreDNS starts.

Key terms

  • kubeadm: official bootstrap tool for Kubernetes clusters.
  • CRI: Container Runtime Interface, the API between kubelet and runtime.
  • CNI: Container Network Interface, plugin API for Pod networking.
  • Static Pod: Pod managed directly by the kubelet from a manifest file.
  • Bootstrap token: short-lived credential used by kubeadm join.
  • Discovery token CA cert hash: SHA-256 of the CA public key, used to verify the API server.
  • kubeconfig / context: client configuration and a named cluster+user+namespace selection.

Summary checklist

  • Swap off, overlay and br_netfilter loaded, ip_forward enabled, all persisted.
  • containerd (or CRI-O) installed with the systemd cgroup driver.
  • Ports 6443, 2379-2380, 10250, 10257, 10259 (control plane) and 10250, 30000-32767 (workers) allowed.
  • kubeadm init run with a non-overlapping --pod-network-cidr and, for future HA, --control-plane-endpoint.
  • kubeconfig copied to ~/.kube/config; CNI plugin applied.
  • Workers joined; expired token replaced with kubeadm token create --print-join-command.
  • Control plane changes made in /etc/kubernetes/manifests, backups stored elsewhere.
  • The right context selected before every task.

Official sources