Lesson 12 of 32 · Level 2 — Operator
Upgrades & version skew
Upgrade a kubeadm cluster one minor version at a time without downtime: the order of operations, the version-skew rules that make it safe, and what to check before you start.
Why upgrades matter
Kubernetes releases a new minor version roughly three times a year, and each minor gets about a year of patch support. Fall behind and you lose security fixes. kubeadm clusters also renew their certificates during upgrades, so never upgrading eventually means expired certificates.
Upgrading a cluster is like renovating a busy hospital. You can't close it. So you renovate one ward at a time: move the patients (pods) to other wards, fix the empty ward, bring patients back, then move to the next. And the head office (control plane) gets renovated first, so it always understands the newest paperwork the wards will send.
The version-skew rules
These rules are what make a rolling upgrade possible:
| Component | Allowed relative to the kube-apiserver |
|---|---|
| Other kube-apiservers (HA) | Within 1 minor of each other |
| kube-controller-manager, kube-scheduler | Same or 1 minor older, never newer |
| kubelet | Same or up to 3 minors older (up to 2 before v1.28); never newer |
| kubectl | Within 1 minor (older or newer) |
The rule to remember: nothing may be newer than the API server. So the order is always control plane first, then kubelets, one minor at a time.
Before you touch anything
- Read the release notes for the target version, especially removed APIs and changed defaults.
- Back up etcd (next lesson). An upgrade is exactly when you want a fresh snapshot.
- Find deprecated API usage. The API server counts requests to deprecated APIs. Here something still calls an API that 1.32 removes:
$ kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
apiserver_requested_deprecated_apis{group="flowcontrol.apiserver.k8s.io",removed_release="1.32",resource="flowschemas",subresource="",version="v1beta3"} 1
- Check PodDisruptionBudgets that could block drains:
kubectl get pdb -A. - Update the package repository.
pkgs.k8s.iohas one repository per minor version, so changev1.31tov1.32in/etc/apt/sources.list.d/kubernetes.liston every node before installing new packages.
Step 1: the first control-plane node
$ sudo apt-mark unhold kubeadm
$ sudo apt-get update && sudo apt-get install -y kubeadm='1.32.x-*'
$ sudo apt-mark hold kubeadm
$ sudo kubeadm upgrade plan
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT NODE CURRENT TARGET
kubelet cp1 v1.31.x v1.32.x
...
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.32.x
$ sudo kubeadm upgrade apply v1.32.x
...
[upgrade/successful] SUCCESS! Your cluster was upgraded to "v1.32.x". Enjoy!
Replace 1.32.x with the exact patch version you chose. upgrade apply rewrites the static pod manifests (API server, controller manager, scheduler, etcd), which the kubelet restarts one by one. It also renews the certificates.
Other control-plane nodes: upgrade the kubeadm package, then run sudo kubeadm upgrade node (not apply).
Step 2: each node's kubelet, one node at a time
Control-plane nodes first, then workers. For each node:
$ kubectl drain w1 --ignore-daemonsets --delete-emptydir-data
node/w1 cordoned
evicting pod default/web-6d4b9c8f7b-4hx9t
...
node/w1 drained
On the node itself (workers also run sudo kubeadm upgrade node first, to refresh the kubelet config):
$ sudo apt-mark unhold kubelet kubectl
$ sudo apt-get install -y kubelet='1.32.x-*' kubectl='1.32.x-*'
$ sudo apt-mark hold kubelet kubectl
$ sudo systemctl daemon-reload && sudo systemctl restart kubelet
Back on a machine with kubectl:
$ kubectl uncordon w1
$ kubectl get nodes
NAME STATUS ROLES VERSION
cp1 Ready control-plane v1.32.x
w1 Ready <none> v1.32.x
w2 Ready <none> v1.31.x ← next
cordon vs drain
cordon marks a node unschedulable (no new pods). drain cordons it and evicts the existing pods, respecting PodDisruptionBudgets. DaemonSet pods are skipped (--ignore-daemonsets) because they belong on every node anyway.
PodDisruptionBudgets: your upgrade safety net
A PDB tells Kubernetes how many pods of an app must stay up during voluntary disruptions such as drains:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
spec:
minAvailable: 2 # never evict below 2 ready pods
selector:
matchLabels:
app: web
With 3 replicas, drain evicts one pod, waits for its replacement to be Ready elsewhere, then continues. A PDB with minAvailable equal to the replica count (or maxUnavailable: 0) blocks the drain forever. That's a classic stuck upgrade.
Try it: an upgrade rehearsal
On your kubeadm lab from lesson 09 (or rebuild it on the older version first):
- Deploy
webwith 3 replicas and the PDB above. - Upgrade the control plane with
kubeadm upgrade planandapply. - Drain w1 in one terminal while running
kubectl get pods -l app=web -o wide -win another. Watch pods move while at least 2 stay Ready. - Set the PDB to
minAvailable: 3and try draining w2. Observe the retry loop, then fix it.
Going deeper: upgrades at work
- Surge instead of drain-in-place: in cloud or VM environments it's often safer to add new nodes on the new version and remove old ones (immutable nodes). Managed services and Cluster API work this way.
- Add-ons have their own compatibility matrices: CNI, CSI drivers, ingress controllers and service meshes. Check them before the control plane moves.
- Watch during the upgrade: API server error rates, etcd leader changes, and pods stuck
PendingorTerminating. - Rollback reality: control-plane downgrades are not supported. Your rollback plan is the etcd backup plus rebuilt control-plane nodes. That's why the backup comes first.
Recap
- One minor version at a time, control plane first, then kubelets node by node.
- Nothing newer than the API server; kubelets may lag a few minors.
- Before starting: release notes, etcd backup, deprecated APIs, PDBs, package repo updated.
- drain → upgrade kubelet → restart → uncordon, and let PDBs protect availability.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.