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
kubeadmis run: container runtime, swap, kernel modules, sysctl settings, open ports, unique identity. - Run
kubeadm initwith 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:
hostnamectl --static
ip link
sudo cat /sys/class/dmi/id/product_uuidContainer 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:
sudo swapoff -a
# make it permanent: comment the swap line in /etc/fstab
sudo sed -i '/ swap / s/^/#/' /etc/fstabSwap 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:
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 --systemoverlay 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:
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubeletAfter 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:
[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 1Reasoning, step by step.
- Read the error names.
SwapandFileContent--proc-sys-net-ipv4-ip_forwardare two independent failures; kubeadm lists all fatal ones at once. - Fix swap:
sudo swapoff -a, then comment the entry in/etc/fstabso the fix survives a reboot. - Fix forwarding: create
/etc/sysctl.d/k8s.confwithnet.ipv4.ip_forward = 1and runsudo sysctl --system. - Re-run
kubeadm init. Do not use--ignore-preflight-errors=allto 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:
sudo kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--apiserver-advertise-address=192.168.10.11--pod-network-cidrreserves 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-addressis the IP the API server advertises. If the machine has several interfaces, set it explicitly.--service-cidr(default10.96.0.0/12) is the virtual IP range for Services.--kubernetes-versionpins the control plane version (defaults to the latest stable matching the kubeadm binary).--control-plane-endpointsets 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-certsuploads 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:
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/12Run 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>:
- preflight: checks swap, ports, runtime, CPU and memory.
- certs: creates the CA and certificates in
/etc/kubernetes/pki(API server, kubelet client, front-proxy, etcd, service account keys). - kubeconfig: writes
admin.conf,kubelet.conf,controller-manager.conf,scheduler.conf. - etcd and control-plane: write static Pod manifests in
/etc/kubernetes/manifests. - kubelet-start: writes the kubelet configuration and restarts it; the kubelet notices the manifests and starts the control plane Pods.
- upload-config, mark-control-plane: store configuration in ConfigMaps in
kube-systemand taint the nodenode-role.kubernetes.io/control-plane:NoSchedule, so ordinary workloads are not scheduled there. - 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:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/configThen install a CNI plugin by applying its manifest, for example kubectl apply -f <cni-manifest>.yaml, and watch the nodes become Ready. Confirm with:
kubectl get nodes
kubectl get pods -n kube-systemJoining nodes
At the end of kubeadm init, a ready-made command is printed:
sudo kubeadm join 192.168.10.11:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:3f4a...e91Two 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:
sudo kubeadm token list
sudo kubeadm token create --print-join-command # new token and the full commandIf you only lost the hash, compute it from the CA:
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.
- The error is about trust, not connectivity: the TCP connection to port 6443 works.
- 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.
- On the new control plane run
kubeadm token create --print-join-commandto get a matching token and hash. - On the worker, wipe the stale state first:
sudo kubeadm reset -f, then run the new join command. - Verify with
kubectl get nodeson 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.
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).- Create
/etc/kubernetes/audit-policy.yamlwith a validaudit.k8s.io/v1Policy and a nonempty rules list, such asrules: [{level: Metadata}]. Add--audit-policy-file=/etc/kubernetes/audit-policy.yamland--audit-log-path=/var/log/kubernetes/audit/audit.logunder the API server command. Mount the policy file read-only and the log directory writable, with matchinghostPathvolumes. The container must be able to read the policy and write the log. - Save. The kubelet recreates the Pod; the API is unavailable for a short time, so
kubectlmay fail for 30-60 seconds. - Check with
sudo crictl ps | grep kube-apiserver. If it keeps restarting, readsudo 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.
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 dataOn 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:
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.confCurrent 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
NotReadyright 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 -aormodprobewithout 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/manifestsbecome live Pods or crash the kubelet's parsing; keep them elsewhere. - Wrong context. Always run
kubectl config use-contextas 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 tokenand the ports table live. Setalias k=kubectlandexport 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.
- What is wrong with the Pod CIDR?
- What must change now so that adding control plane nodes later is possible?
- Nodes show
NotReadyand CoreDNS isPendingafter init. Is the init broken?
Solution, commented.
- The Pod range
192.168.0.0/16contains the node subnet, so Pod and node addresses overlap and routing becomes ambiguous. Choose a range such as10.244.0.0/16that does not overlap with node or Service ranges. - 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. - 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,
overlayandbr_netfilterloaded,ip_forwardenabled, all persisted. - containerd (or CRI-O) installed with the
systemdcgroup driver. - Ports 6443, 2379-2380, 10250, 10257, 10259 (control plane) and 10250, 30000-32767 (workers) allowed.
-
kubeadm initrun with a non-overlapping--pod-network-cidrand, 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
-
Installing kubeadm: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/ (opens in a new tab)
-
Creating a cluster with kubeadm: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/ (opens in a new tab)
-
Ports and protocols: https://kubernetes.io/docs/reference/networking/ports-and-protocols/ (opens in a new tab)
-
Organizing cluster access with kubeconfig: https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ (opens in a new tab)
-
CKA curriculum: https://github.com/cncf/curriculum (opens in a new tab)
-
Exam FAQ: https://docs.linuxfoundation.org/tc-docs/certification/faq-cka-ckad-cks (opens in a new tab)
-
CKA curriculum domains and practical objectives (opens in a new tab)
