Every component has a version
The problem. Kubernetes ships a new minor version (1.34 -> 1.35) about every four months and supports each for about a year. Upgrading in the wrong order, or two steps at once, leaves components that cannot talk to each other. The version skew policy (the official rules for which versions may run together) tells you the order.
What you need to know already: the control plane and the kubelet (15.5, 15.7), kubeadm and the per-minor package repositories (18.6), versions as major.minor.patch.
A cluster is not "on 1.34". Each component has its own version and they are upgraded at different moments:
$ kubectl version
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.34.1
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cp-1 Ready control-plane 12d v1.34.1
worker-1 Ready <none> 12d v1.34.1
worker-2 Ready <none> 12d v1.34.1
- Server Version is the kube-apiserver.
- Client Version is the kubectl binary you ran.
- the VERSION column of get nodes is the kubelet on each node - not the control plane, not the apiserver.
kubeadm version -o short(on a node) is the kubeadm package.- Kustomize in the kubectl output is a YAML-templating tool built into kubectl (
kubectl apply -k); ignore it here. - control-plane images are in the manifests:
sudo grep image: /etc/kubernetes/manifests/*.yaml.
The skew policy
From kubernetes.io "Version Skew Policy" (current as of the 1.3x releases). Numbers are minor versions (1.34 -> 1.35 is one minor); patch versions (1.34.1 -> 1.34.4) may always be mixed.
| component | rule relative to kube-apiserver |
|---|---|
| kube-apiserver (HA, several instances) | newest and oldest within 1 minor |
| kubelet | never newer; up to 3 minors older |
| kube-proxy | never newer; up to 3 older (and within 3 of the kubelet next to it) |
| kube-controller-manager, kube-scheduler | never newer; at most 1 older (normally equal) |
| kubectl | within 1 minor, older or newer |
Examples with the apiserver at 1.35:
- kubelet 1.35, 1.34, 1.33, 1.32 - supported. kubelet 1.36 - not (newer than the apiserver).
- scheduler 1.34 - supported during an upgrade; scheduler 1.33 - not.
- kubectl 1.34, 1.35, 1.36 - supported; kubectl 1.33 - not. kubectl warns you:
Warning: version difference between client (1.33) and server (1.35) exceeds the supported minor version skew of +/-1
(The kubelet window was two minors before 1.28; it is three now. The policy page is the source of truth - check it when you plan a real upgrade.)
What the rules imply
The control plane goes first. Every component must be not newer than the apiserver, so the apiserver is always the first thing upgraded and the kubelets the last. Inside the control plane: apiserver, then controller-manager and scheduler (kubeadm upgrade apply does exactly that order).
One minor at a time. kubeadm refuses to skip:
[upgrade/version] FATAL: the --version argument is invalid due to these errors:
- Specified version to upgrade to "v1.36.1" is too high; kubeadm can upgrade only 1 minor version at a time
1.33 -> 1.35 means 1.33 -> 1.34 -> 1.35: two full upgrades. The generous kubelet skew is what makes this bearable - you can upgrade the control plane twice and roll the nodes once, as long as they never fall more than three behind.
Nodes can lag, briefly. The window is for during an upgrade and for node pools you replace rather than upgrade in place (managed clouds swap whole node images). It is not a place to live: features and bug fixes need the kubelet too, and the next control plane upgrade shrinks the window.
kubeadm itself goes first on each node. kubeadm can only upgrade to a version it knows; the command fails with Specified version to upgrade to "v1.35.3" is higher than the kubeadm version "v1.34.1". Upgrade kubeadm first using the tool you used to install kubeadm.
The order, for this cluster (1.34 -> 1.35)
- cp-1: repository line to
v1.35,apt-get update, unhold + install kubeadm 1.35.x + hold. - cp-1:
kubeadm upgrade plan, thenkubeadm upgrade apply v1.35.x- control-plane static pods, etcd if needed, CoreDNS, kube-proxy, certificates renewed. Now the apiserver says 1.35, every kubelet still 1.34 (allowed). - cp-1: drain it, upgrade kubelet + kubectl packages,
daemon-reload, restart kubelet, uncordon.get nodesshows cp-1 v1.35. - each worker, one at a time: repository line, kubeadm package,
kubeadm upgrade node, drain, kubelet + kubectl packages, daemon-reload, restart kubelet, uncordon. Wait for Ready before the next one.
The next lesson walks through each command and its output.
What you can now do
- Read every component's version (
kubectl version,get nodes,kubeadm version). - Say whether a combination is supported, and why the control plane goes first, one minor at a time.