kubeadm
kubeadm is the tool that turns a set of prepared Linux machines into a real Kubernetes control plane and a set of joined nodes — nothing more, and deliberately nothing less. Kubernetes architecture already named every component kubeadm stands up; CKA Domain 1 already walked the exam-shaped version of building one. This page goes a level deeper into kubeadm itself: the phase pipeline init actually runs (and how to re-run just one phase when something breaks), the typed --config API that replaces a wall of flags once you're past a single demo node, the full join/upgrade/reset/token/certs command family, and — the part that quietly decides whether your cluster is still trustworthy in eleven months — the three-CA certificate tree kubeadm builds on init and what kubeadm certs renew actually does and doesn't do when one of those certificates is about to expire.
Imagine hiring a crew that only does one job: pour the foundation and raise the load-bearing walls of a house, in the one order that actually works — foundation before walls, walls before roof. Before they leave, they also cut you two keys: a front-door key for yourself, and a spare with instructions for anyone else you want to let live there later. What they don't do is pick your paint colors, wire the internet, or come back next year to check the locks still work — that's on you, on a schedule, forever. kubeadm is that crew. It builds exactly the control plane a cluster needs, in the one safe order, hands you a working admin.conf and a join command, and then steps back. Networking, storage, and remembering that every lock it installed quietly expires in about a year — those are jobs it handed you on the way out, not jobs it forgot.
What kubeadm is, and where its responsibility stops
☺ Like you're 10: kubeadm builds the control plane and hooks nodes onto it — it doesn't create the machines, install networking, or manage the cluster forever after.
kubeadm is a CNCF subproject, part of Kubernetes itself rather than a third-party add-on, and its own docs state the scope directly: it is not a tool to set up infrastructure. It does not create VMs, provision cloud accounts, or configure DNS — that's Terraform's job, or your cloud console's. It does not install a CNI plugin, so a freshly-bootstrapped cluster sits with every node NotReady until you apply one yourself — deliberately, because CNI choice is exactly the kind of decision kubeadm refuses to make on your behalf. And past the initial init/join, its ongoing responsibility stays just as narrow: version upgrades and certificate renewal, full stop. Cluster-level add-ons — an ingress controller, cert-manager, a CSI driver — are typically installed afterward with Helm or Kustomize, per CKA Domain 1's own Helm-and-Kustomize-for-cluster-components competency; kubeadm has no opinion on any of it, and no daemon watching the cluster between the commands you actually run.
That narrowness is the whole design, and it's why this page and CKA Domain 1 are meant to be read together rather than as substitutes for each other: that page walks kubeadm through the lens of the curriculum's own competencies — infrastructure prep, HA topologies, version skew — and this one stays on the tool underneath, going deeper on the phase pipeline, the config API, and the certificate tree than a domain-shaped page needs to.
kubeadm being "just" a bootstrapper is exactly why EKS, GKE, and AKS don't run it in front of you. Managed control planes replace the whole job kubeadm does with the cloud vendor's own internal tooling, which is why you never see a kube-apiserver Pod sitting in kube-system on those platforms. Self-managed clusters — on-prem, air-gapped, or hand-rolled on cloud VMs — are kubeadm's actual home turf, and the reason CKA leans on it so heavily as an exam tool.
The init pipeline, as phases — not one atomic step
☺ Like you're 10: kubeadm init is really a fixed sequence of smaller steps, and you can re-run just one of them if something specific broke.
kubeadm init looks like a single command, but internally it's a strict pipeline of phases, each doing exactly one well-scoped thing and each individually re-runnable: preflight (swap, cgroup driver, required ports, the container runtime socket — all checked before anything touches disk), certs (the entire PKI tree covered below), kubeconfig (admin.conf, super-admin.conf, kubelet.conf, controller-manager.conf, scheduler.conf), kubelet-start (drops the kubelet's own config and starts it as a systemd unit — before any Pod exists to run), control-plane (writes the static Pod manifests for kube-apiserver, kube-controller-manager, and kube-scheduler into /etc/kubernetes/manifests/), etcd (the local static Pod manifest, unless you're pointing at external etcd), upload-config and upload-certs (stash the config and, optionally, the PKI material inside the cluster itself so later nodes can fetch it), mark-control-plane (labels and taints the node), bootstrap-token (creates the token new nodes join with), and finally kubelet-finalize (switches the kubelet from its bootstrap credential to a rotating one) and addon — CoreDNS and kube-proxy, the only two workloads kubeadm installs on your behalf.
kubeadm init phase list # the exact sequence, for your installed version
kubeadm init phase certs all --config kubeadm-config.yaml # re-run just the certs phase
kubeadm init phase control-plane all --config kubeadm-config.yaml # re-run just the static-pod manifestsRe-running one named phase in isolation — rather than tearing the node down and starting over — is the single most useful debugging move when init dies partway through: fix whatever preflight or configuration problem caused the failure, then resume from the phase that actually failed instead of repeating everything that already succeeded.
kubeadm config: a typed API instead of a wall of flags
☺ Like you're 10: past your first demo node, a config file beats remembering fifteen flags in the right order every single time.
Every flag kubeadm init accepts also has a home in a typed configuration object — ClusterConfiguration, InitConfiguration, KubeletConfiguration, and KubeProxyConfiguration — versioned under the kubeadm.k8s.io API group (v1beta3, moving to v1beta4 on newer releases; kubeadm config migrate rewrites an old file forward across that boundary when you upgrade). kubeadm config print init-defaults dumps a fully-populated starting point; from there you edit only what matters — controlPlaneEndpoint, the HA load-balancer address you must set on day one even for a single-node cluster (see the HA section of CKA Domain 1 for why that can't be added later), the pod and service CIDRs, the container runtime socket, and, worth calling out on its own, cgroupDriver — it has to match whatever your container runtime is already using, and a mismatch here produces confusing node-level failures rather than one clean error.
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.31.4
controlPlaneEndpoint: "k8s-lb.internal:6443"
networking:
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/12"
apiServer:
certSANs:
- "k8s-lb.internal"
- "10.0.1.10"
etcd:
local:
dataDir: "/var/lib/etcd"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
criSocket: "unix:///var/run/containerd/containerd.sock"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
serverTLSBootstrap: truekubeadm config print init-defaults > kubeadm-config.yaml
kubeadm config images list --kubernetes-version v1.31.4 # every image init will need
kubeadm config images pull --config kubeadm-config.yaml # pre-pull for an air-gapped node
kubeadm config migrate --old-config old.yaml --new-config new.yaml # across a v1betaN boundary
sudo kubeadm init --config kubeadm-config.yamlCheck that file into the same repository your infrastructure-as-code already lives in, and cluster bootstrap becomes reviewable and repeatable rather than a sequence of flags someone remembers from last time — the same argument Terraform makes for infrastructure generally, applied one layer down, to the layer Terraform itself stops short of.
Joining more nodes: the token and certificate-key handshake
☺ Like you're 10: a worker needs a token to prove it was invited; an extra control-plane node needs that plus a key to unlock the shared certificates too.
kubeadm init ends by printing two ready-to-paste commands, and the difference between them is exactly the difference between a worker and a control-plane node. Both carry a bootstrap --token and a --discovery-token-ca-cert-hash — the hash lets a joining node verify it's actually talking to the real cluster's CA before it trusts anything else the API server tells it, which is the whole point: without that check, a joining node would have to trust an arbitrary endpoint on the network by name alone. A control-plane join additionally carries --control-plane and --certificate-key — a one-time key that unlocks the PKI material --upload-certs stashed, encrypted, inside the cluster during init, since a second control-plane node needs the exact same shared certs as the first, not a fresh set of its own. Miss the two-hour window that upload lives for, and kubeadm init phase upload-certs --upload-certs — already covered in CKA Domain 1 — regenerates it.
# worker node
sudo kubeadm join k8s-lb.internal:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:1234...cafe
# additional control-plane node
sudo kubeadm join k8s-lb.internal:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:1234...cafe \
--control-plane \
--certificate-key 9a1b2c...
# token management, independent of running init again
kubeadm token list # every live bootstrap token, and its TTL
kubeadm token create --ttl 0 --print-join-command # a new, never-expiring token + full command
kubeadm token delete abcdef.0123456789abcdef # revoke one immediatelyDefault tokens live 24 hours, which is deliberate — a bootstrap token is a credential, and a credential nobody remembers to revoke is exactly the kind of gap defense in depth exists to catch before it becomes an incident. --ttl 0 for a permanent token is a real flag and a real footgun outside a short-lived lab: treat it the way you'd treat any other long-lived credential — deliberately, and rarely. Once every node reports Ready, everything past this point is kubectl's job, not kubeadm's.
Certificate management: the tree kubeadm actually builds
☺ Like you're 10: kubeadm doesn't hand you one certificate — it builds three small CAs and issues a whole tree of one-year certificates from them, and only part of that tree is kubeadm's job to renew.
Every certificate kubeadm ever issues traces back to one of three certificate authorities, generated once during the certs phase and never touched again by ordinary operation: kubernetes-ca (/etc/kubernetes/pki/ca.{crt,key}), which signs kube-apiserver's own serving certificate, the apiserver-kubelet-client certificate apiserver uses to call kubelets back, and the client certificate embedded in every control-plane kubeconfig — admin.conf, super-admin.conf on recent kubeadm versions (a break-glass credential still carrying the older, unbound system:masters group, kept separate from admin.conf, which now binds to an ordinary, auditable ClusterRoleBinding instead), controller-manager.conf, and scheduler.conf; etcd-ca (/etc/kubernetes/pki/etcd/ca.{crt,key}), its own separate CA signing etcd's peer and server certificates plus the apiserver-etcd-client certificate apiserver uses to reach etcd — kept apart from kubernetes-ca specifically so that etcd, the one component nothing but apiserver should ever touch, doesn't share a trust root with anything else; and front-proxy-ca, which exists purely for the aggregated-API-server pattern, so an extension API server like metrics-server can prove a request genuinely arrived through the real apiserver rather than from anywhere on the network.
kubeadm certs check-expiration # every cert kubeadm knows about, and days remaining
kubeadm certs renew all # reissue every leaf cert from the SAME CAs
kubeadm certs renew apiserver # or renew just one, by namekubeadm certs renew rewrites the cert and key files on disk and stops there. kubeadm upgrade apply renews certificates as part of its normal flow and restarts the affected static Pods for you; a manual certs renew outside of an upgrade does neither, because kubelet has no mechanism watching a mounted certificate file for changes. Leave it there and the control plane keeps serving the old certificate out of memory until something restarts the container — move the affected manifest out of /etc/kubernetes/manifests/ and back in (or delete the container directly with crictl) to force kubelet to recreate it against the fresh files.
One certificate pair kubeadm's own tree never covers is the kubelet's own identity. A kubelet's client certificate — the one it authenticates to kube-apiserver with — starts from a bootstrap token exactly like a joining node's, then rotates itself continuously by requesting a fresh one through the certificates.k8s.io CertificateSigningRequest API, which kube-controller-manager's built-in csrapproving controller auto-approves for that one specific signer (kubernetes.io/kube-apiserver-client-kubelet) as long as rotateCertificates is set in KubeletConfiguration — kubeadm turns it on by default. A kubelet's serving certificate — the one presented when apiserver proxies kubectl exec/logs/port-forward through to it — is a separate, opt-in story: serverTLSBootstrap: true requests one the same way, but nothing auto-approves that CSR out of the box, which is exactly why metrics-server --kubelet-insecure-tls shows up in so many quickstart guides — it's routing around a serving certificate that was never approved, not a real fix. A dedicated approver like kubelet-csr-approver, or a manual kubectl certificate approve, is the actual one.
This page covers the control plane's own PKI; certificates for workloads running inside the cluster — Ingress TLS, mTLS between services — are a different, application-facing problem with a different toolchain. See DevSecOps' Cryptography & Key Management for cert-manager and ACME automation of those.
"First time I ran kubeadm certs renew all on a cluster six weeks from an apiserver certificate actually expiring, I felt very responsible right up until the next kubectl get pods still failed with an expired-certificate error. Confused everyone in the room for a solid ten minutes — the files on disk were correct, openssl x509 -enddate against them proved it, and kube-apiserver was still serving the old one anyway. renew writes files; it doesn't reach into a running container and tell it to reload them. Now the very next thing I type after any manual certs renew is the restart, every time, before I even check whether it worked."
Upgrading a kubeadm cluster
☺ Like you're 10: nothing in a kubeadm cluster is ever upgraded ahead of kube-apiserver — the whole order follows from that one rule.
CKA Domain 1 already walks the version-skew policy and the full apply-on-first-node, node-on-everyone-else upgrade order in detail, and nothing here changes that. Two things worth adding from the tool's own side: kubeadm upgrade plan is read-only and safe to run at any time — the closest thing to terraform plan a cluster upgrade has, showing exactly what's available before anything actually changes; and, by default, kubeadm upgrade apply renews every certificate it manages as part of the upgrade and restarts the affected static Pods for you — precisely the automatic-restart behavior a manual certs renew doesn't give you. Pass --certificate-renewal=false if you deliberately want to decouple the two, though there's rarely a good reason to.
kubeadm upgrade plan
kubeadm upgrade diff v1.31.4 # preview exactly which static-pod manifests will change
kubeadm upgrade apply v1.31.4 # first control-plane node onlykubeadm reset, and what it deliberately leaves behind
☺ Like you're 10: reset undoes what init did to /etc/kubernetes on this one machine — it does not undo what your CNI plugin, or the rest of the cluster, still remembers.
kubeadm reset reverses roughly what init or join did to the local node: it stops the static Pods, wipes /etc/kubernetes/, and removes the etcd data directory on a node that ran etcd. Three things it deliberately does not do, and the docs are explicit about this: it does not clean up the CNI plugin's own configuration under /etc/cni/net.d/ or the network interfaces it created, which is why a node headed for a different cluster should usually be rebooted or manually cleaned first, not just reset; it does not flush the iptables or IPVS rules kube-proxy programmed, so stale NAT rules can outlive the node's actual cluster membership; and it never touches the rest of the cluster at all — the Node object for the machine you just reset is still sitting in etcd until someone runs kubectl delete node from a control-plane node that's still up. Skip that step and the cluster keeps reporting a Node that will never send another heartbeat.
sudo kubeadm reset --force
sudo rm -rf /etc/cni/net.d
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F
kubectl delete node worker-3 # from a control-plane node — reset never does this for youkubeadm vs. the alternatives
☺ Like you're 10: kubeadm is the vendor-neutral, do-it-yourself option — every alternative trades away some of that control for something else: less to type, or no cluster to operate at all.
| Option | Model | Best when | Costs you |
|---|---|---|---|
| kubeadm | Manual, vendor-neutral bootstrap; you run every command | Learning the real mechanics, or a bespoke on-prem/edge cluster no packaged distro fits | You own every command, every upgrade, every cert renewal, forever |
| kOps | Declarative cluster lifecycle tool, mostly AWS (growing GCP/Azure support), built on kubeadm underneath | Repeatable, versioned clusters on a supported cloud without hand-running kubeadm | Less control over the exact bootstrap sequence; tied to kOps' own release cadence |
| kubespray | Ansible playbooks that drive kubeadm across an inventory you already manage | You're already an Ansible shop and want kubeadm's exact output, just automated | Ansible's own learning curve and playbook maintenance, on top of kubeadm's |
| k3s / RKE2 | A single lightweight binary with its own, simpler bootstrap — not kubeadm underneath | Edge, IoT, or resource-constrained clusters where kubeadm's footprint is overkill | A different, though CNCF-conformant, distribution — not identical component-for-component to upstream |
| Talos Linux | An immutable, API-managed OS purpose-built to run Kubernetes — no SSH, no shell, config only | Maximum hardening and a minimal attack surface, and losing general-purpose SSH is fine | A genuinely different operational model to learn, not just a different install command |
| EKS / GKE / AKS | Managed control plane; the vendor's own bootstrap tooling runs entirely behind the API | Zero control-plane operations, at the cost of a bill and less to learn about the mechanics | No hands-on bootstrap or cert-management experience — exactly the muscle CKA tests |
That last row is worth sitting with if you're studying for CKA specifically: the exam tests the muscle managed Kubernetes exists to make you never need. Practicing a real init, an actual join, a genuine certs renew, and a real upgrade on disposable VMs — not just reading about them — is the difference between passing and passing comfortably. See this course's certifications hub for CKA alongside CKAD and CKS.
Two disposable VMs — a local hypervisor (Multipass, Vagrant) or two cheap cloud instances both work — with swap off, containerd installed, and the kernel networking prerequisites CKA Domain 1 lists. Run kubeadm init on the first, apply a CNI plugin, then kubeadm join the second as a worker. Once both nodes show Ready, run kubeadm certs check-expiration and note every date. Then deliberately renew one certificate with kubeadm certs renew apiserver, run check-expiration again to confirm the new date — and try kubectl get nodes before you restart anything. Watch it fail with an expired-certificate error against a certificate that, by the date on disk, isn't expired at all. That gap is this entire page in one command.
Benny: Cluster's up, both nodes Ready, CoreDNS is running. That was one init, one join, and about four minutes.
Professor Owl: And a whole certificate tree you didn't have to think about once — three CAs, a dozen leaf certs, all signed correctly, all with a one-year clock now running whether you're watching it or not.
Gizmo: A year is basically forever. I'll set a reminder. Probably.
Timmy: "Probably" is how a cluster goes down on a random Tuesday with every controller suddenly unable to talk to the apiserver. kubeadm certs check-expiration, on a calendar — not a hope.
Gizmo: Fine — renew, restart nothing, ship it. They're just files, right?
Timmy: Files a running container already read into memory. New files on disk don't help until something restarts that container against them — ask me how I know.
Professor Owl: Which is exactly why kubeadm upgrade apply restarts the static Pods for you automatically, and a bare certs renew doesn't. Same tool, two different levels of hand-holding — know which one you're using.
1. Name kubeadm's two ongoing responsibilities after init/join finish, and name two things it deliberately never does, even on day one. 2. kubeadm init fails partway through the control-plane phase. What's the fastest way to retry just that phase without tearing the node down? 3. Which CA signs apiserver-etcd-client.crt, and why is that CA kept separate from kubernetes-ca in the first place? 4. You run kubeadm certs renew all on a live control-plane node. What changes immediately, what doesn't, and what fixes the gap? 5. A worker's kubelet client certificate rotates on its own without you ever running a kubeadm command. What API makes that possible, and what auto-approves the resulting CSR? 6. Name two things kubeadm reset explicitly does not clean up on the node it ran on. 7. In one sentence, why does CKA weight hands-on kubeadm practice so heavily compared to a managed-Kubernetes user's day-to-day experience?
Check your answers
- Version upgrades and certificate renewal — that's it. It never provisions the underlying machines, and it never installs a CNI plugin; a fresh cluster sits
NotReadyuntil you apply one yourself. kubeadm init phase control-plane all --config kubeadm-config.yaml(or the specific sub-phase) re-runs just that phase in isolation, without repeatingpreflight,certs, and everything else that already succeeded.- etcd-ca signs it. It's kept separate from kubernetes-ca so that etcd — the one component nothing but kube-apiserver should ever reach — doesn't share a trust root with the rest of the cluster's certificates.
- The cert and key files on disk change immediately. The running control-plane containers keep serving the OLD certificate from memory, because nothing restarted them. Moving the affected static-pod manifest out of
/etc/kubernetes/manifests/and back in (or deleting the container viacrictlso kubelet recreates it) is what actually applies the renewal. - The
certificates.k8s.ioCertificateSigningRequest API. kube-controller-manager's built-incsrapprovingcontroller auto-approves that specific signer for kubelet client certs — not for kubelet serving certs, which need a separate approver. - It doesn't clean up the CNI plugin's own configuration under
/etc/cni/net.d/, and it doesn't flush the iptables/IPVS rules kube-proxy programmed — both need manual cleanup, or a reboot, before the node is safe to reuse. - Because managed Kubernetes hides the entire bootstrap-and-certificate lifecycle behind the vendor's own tooling — exactly the hands-on muscle (building a control plane, joining nodes, renewing certs, upgrading in the right order) a managed-Kubernetes user never has to build, and that CKA specifically tests.