Cluster upgrades
Version skew rules, switching the pkgs.k8s.io repo, kubeadm upgrade plan, apply and node, draining and uncordoning, and renewing kubeadm certificates.
Exam tasks: 1.4 (manage the lifecycle of Kubernetes clusters)
The decision: in what order do you upgrade which packages on which nodes, so the cluster moves up one minor version without breaking the version skew policy or dropping workloads?
Order of operations
- Control plane first, then workers. The kubelet must never be newer than the API server.
- One minor at a time. Going from 1.33 to 1.35 is two upgrades: 1.33 to 1.34, then 1.34 to 1.35. Patch versions within a minor can be skipped.
kubeadm upgradeupgrades the static Pods (API server, controller manager, scheduler, etcd), CoreDNS and kube-proxy. It never upgrades the kubelet package or your CNI plugin.
Version skew you must respect
| Component | Allowed relative to kube-apiserver |
|---|---|
| Other kube-apiserver instances (HA) | Within one minor version of each other |
| kube-controller-manager, kube-scheduler | Same minor or one older, never newer |
| kubelet | Up to three minors older, never newer |
| kube-proxy | Up to three minors older, never newer |
| kubectl | One minor newer or older |
Exam signal
"Upgrade the control plane only" means the workers keep their old kubelets, which is fine within three minors.
"Upgrade the whole cluster" means every node's kubelet too, and k get nodes should show the new version on each
one when you're done.
First control plane node
ssh cp-a
# 1. switch the repo to the target minor (repeat on every node before its turn)
sudo sed -i 's#/v1.34/#/v1.35/#' /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
apt-cache madison kubeadm | head -3 # find the exact package version, e.g. 1.35.2-1.1
# 2. kubeadm first
sudo apt-mark unhold kubeadm
sudo apt-get install -y kubeadm='1.35.2-1.1'
sudo apt-mark hold kubeadm
kubeadm version -o short
# 3. plan and apply
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.35.2
# 4. kubelet and kubectl, with the node drained
k drain cp-a --ignore-daemonsets
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet='1.35.2-1.1' kubectl='1.35.2-1.1'
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload && sudo systemctl restart kubelet
k uncordon cp-aupgrade planchecks that the cluster is healthy and lists the versions you can move to. Read it; it also tells you if a component config needs manual attention.upgrade applyrenews the certificates kubeadm manages on that node unless you pass--certificate-renewal=false.- Additional control plane nodes run
sudo kubeadm upgrade nodeinstead ofapply, then the same kubelet steps. Do them one at a time; addons such as CoreDNS are upgraded after the last control plane finishes.
apt-get upgrade or forgetting the hold
apt-get upgrade on a node with unheld packages can jump the kubelet ahead of the control plane, and a version
that isn't in the repo you configured simply isn't found. Upgrade with pinned versions, and re-hold the packages so
the next routine patch run doesn't change them behind your back.
Worker nodes
ssh node-b
sudo sed -i 's#/v1.34/#/v1.35/#' /etc/apt/sources.list.d/kubernetes.list && sudo apt-get update
sudo apt-mark unhold kubeadm && sudo apt-get install -y kubeadm='1.35.2-1.1' && sudo apt-mark hold kubeadm
sudo kubeadm upgrade node # refreshes the local kubelet configuration
exit
k drain node-b --ignore-daemonsets --delete-emptydir-data # from wherever kubectl works
ssh node-b
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet='1.35.2-1.1' kubectl='1.35.2-1.1'
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload && sudo systemctl restart kubelet
exit
k uncordon node-b
k get nodes # node-b now shows v1.35.2Drain, cordon and uncordon
| Command | Effect |
|---|---|
k cordon node-b | Marks the node unschedulable. Running Pods stay |
k drain node-b --ignore-daemonsets | Cordons, then evicts Pods through the Eviction API, honouring PodDisruptionBudgets |
--delete-emptydir-data | Needed when Pods use emptyDir; that data is lost |
--force | Also deletes bare Pods not owned by a controller. They don't come back |
k uncordon node-b | Schedulable again. Existing Pods don't move back on their own |
- A drain that hangs is usually blocked by a PodDisruptionBudget that can't be satisfied, for example a
single-replica Deployment with
minAvailable: 1. Scale it up or wait, don't delete the PDB unless the task says so. - DaemonSet Pods are never evicted, which is why
--ignore-daemonsetsis required.
Certificates
kubeadm-issued certificates are valid for one year. Upgrading at least once a year renews them automatically.
sudo kubeadm certs check-expiration
sudo kubeadm certs renew all # or renew apiserver, renew admin.conf, ...
# static Pods don't reload certificates on their own: restart them
sudo mkdir -p /root/mf-hold
sudo mv /etc/kubernetes/manifests/*.yaml /root/mf-hold/ && sleep 20
sudo mv /root/mf-hold/*.yaml /etc/kubernetes/manifests/renew all also rewrites admin.conf; copy it to ~/.kube/config again if you use that.
Moving the manifests out makes the kubelet stop those static Pods; moving them back starts them with the new
certificates. Use an empty folder outside /etc/kubernetes/manifests, and on an HA cluster do one node at a time.
Legacy: use per-minor pkgs.k8s.io repos instead
Older guides upgrade with apt-get install kubeadm=1.2x.y-00 from a single kubernetes-xenial repo. That repo
is gone. With pkgs.k8s.io you first change the minor version in the repo URL, and package versions look like
1.35.2-1.1.
Scenarios
kubeadm only upgrades one minor version at a time, so 1.33 to 1.35 needs a hop through 1.34. Kubelets may stay
behind (up to three minors) but must never be ahead of the API server, which rules out upgrading them first.
upgrade node on its own never upgrades the first control plane.
With two replicas, one can be evicted and the budget still holds, so the drain proceeds without downtime. --force
only affects Pods without a controller and doesn't override a PDB. Deleting the Pod by hand gets around the budget
and causes an outage. Upgrading without draining risks the workload during the kubelet restart.
Drill
kubectl config use-context cka-up
The cluster has control plane up-cp and worker up-w1, both on v1.34. Upgrade the control plane node only
(control plane components, kubelet and kubectl) to the latest v1.35 patch available in the v1.35 package repo.
Leave up-w1 untouched. Make sure up-cp is schedulable again at the end.
ssh up-cp
sudo sed -i 's#/v1.34/#/v1.35/#' /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
apt-cache madison kubeadm | head -1 # e.g. 1.35.3-1.1
sudo apt-mark unhold kubeadm
sudo apt-get install -y kubeadm='1.35.3-1.1'
sudo apt-mark hold kubeadm
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.35.3 -y
k drain up-cp --ignore-daemonsets
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet='1.35.3-1.1' kubectl='1.35.3-1.1'
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload && sudo systemctl restart kubelet
k uncordon up-cp
exitVerify:
k get nodes # up-cp v1.35.3 Ready (not SchedulingDisabled), up-w1 still v1.34.x
k version # server v1.35.3
k get pods -n kube-system # all RunningFurther reading
Installing a cluster with kubeadm
Preparing nodes (swap, IP forwarding, containerd and the cgroup driver, pkgs.k8s.io packages), kubeadm init and its config file, installing a CNI, joining workers and regenerating join commands.
etcd backup and restore
Finding etcd's endpoint and certificates, saving a snapshot with etcdctl, checking it and restoring it with etcdutl, and pointing the etcd static Pod at the restored data directory.