Kubernetes Version Policy
Summary
- A version policy is a written commitment to which Kubernetes minors your org runs, how fast it upgrades, and who is accountable when it slips.
- Insight: Kubernetes patches only the three most recent minors. Falling behind is not a scheduling problem, it is a security posture problem.
- Key Concepts: supported window (three minors), kubelet version skew, upgrade calendar, control-plane-first ordering, EOL date.
- When to Use: The moment you run more than one cluster, or any cluster you cannot rebuild from scratch in an afternoon.
- Limitations: A policy does not upgrade anything. It makes the cost of not upgrading visible and assigned.
Recipe
- Declare a target minor and a floor minor in writing. Today: target 1.36.2, floor 1.34.
- Publish an upgrade calendar tied to the upstream cadence of roughly three minors per year.
- Upgrade the control plane first, then node pools, never the reverse.
- Keep kubelets at most one minor behind the API server as policy.
- Gate every window on a pre-upgrade check: deprecated APIs, add-on compatibility, PDB sanity.
- Track EOL dates as a due date on a real ticket, not a calendar reminder.
Working Example
Check what you actually run before deciding anything.
kubectl version -o yaml
kubectl get nodes -o custom-columns=NODE:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,RUNTIME:.status.nodeInfo.containerRuntimeVersionThe runtime column should read containerd://... on every node. Docker Engine is your build tool, not your node runtime.
Encode the policy as data your automation can read.
apiVersion: v1
kind: ConfigMap
metadata:
name: version-policy
namespace: platform-system
data:
target-minor: "1.36"
target-patch: "1.36.2"
floor-minor: "1.34"
eol-notes: "1.33 is end-of-life - no patches, no CVE fixes"
upgrade-window: "second Tuesday, 14:00-18:00 UTC"
max-kubelet-skew: "1"Then fail CI when a cluster drifts below the floor.
#!/usr/bin/env bash
set -euo pipefail
FLOOR_MINOR=34
SERVER_MINOR=$(kubectl version -o json | jq -r '.serverVersion.minor' | tr -d '+')
if [ "${SERVER_MINOR}" -lt "${FLOOR_MINOR}" ]; then
echo "FAIL: control plane at 1.${SERVER_MINOR}, below floor 1.${FLOOR_MINOR}"
exit 1
fi
echo "OK: control plane at 1.${SERVER_MINOR}"A node upgrade should always drain politely first.
kubectl drain node-a --ignore-daemonsets --delete-emptydir-data --timeout=600s
# replace the node image / upgrade the kubelet here
kubectl uncordon node-aDeep Dive
The three-minor support window
Upstream Kubernetes patches only the three most recent minor releases.
With 1.36 current, that covers 1.36, 1.35, and 1.34. 1.33 is EOL and will not receive fixes for newly disclosed CVEs.
Running EOL is not "slightly behind". It is running software with no security response process behind it.
Why an N-1 policy, and what the real limit is
Recent Kubernetes supports kubelets up to three minors older than the API server, but that width is a migration allowance, not a target.
A policy of one minor of skew gives you a boring, heavily tested combination and keeps the wider window as emergency headroom.
Never run a kubelet newer than the API server. That direction is unsupported and fails in ways that are hard to diagnose.
Control plane first, always
The API server must be at or ahead of every component that talks to it.
Upgrading nodes first produces newer kubelets against an older API server, which is exactly the unsupported direction.
Managed control planes enforce this ordering for you; self-managed clusters will happily let you get it wrong.
Building the calendar
Upstream ships roughly three minors a year, so a minor lands about every four months.
Pick a lag you can defend. One minor behind current is a common choice for regulated orgs; tracking current once the .2 patch lands is fine for teams with strong test coverage.
Whatever you pick, the calendar must produce at least as many upgrades per year as upstream produces releases, or you drift into EOL by arithmetic alone.
The pre-upgrade gate
Every window runs the same checklist before the first node moves.
Scan manifests for APIs removed in the target minor. Confirm every add-on and operator publishes support for the target. Verify PodDisruptionBudgets allow at least one pod to move.
helm list -A -o json | jq -r '.[] | "\(.namespace)/\(.name) \(.chart)"'
kubectl get pdb -AA PDB with minAvailable equal to the replica count will block a drain forever. That is the single most common reason a window expires with nothing upgraded.
Patch versions are a different conversation
Minor upgrades are a project. Patch upgrades such as 1.36.1 to 1.36.2 should be routine and near-automatic.
Separate the two in policy so patch fixes are not held hostage by minor-upgrade planning.
Gotchas
A PDB that can never be satisfied. A single-replica Deployment with minAvailable: 1 blocks every drain. Fix it with maxUnavailable: 1 or a second replica.
Node pools drifting silently. Autoscalers can create nodes from a stale node image. Pin the node image version in the node group config, not in a runbook.
Add-ons pinned to an old API. An ingress controller or CSI driver without support for the target minor strands the whole upgrade. Check add-ons before the control plane, not after.
Assuming managed means upgraded. Managed control planes have auto-upgrade windows teams disable and then forget. Verify with kubectl version, not with a console setting you remember toggling.
Treating EOL as a soft deadline. There is no grace period. On the EOL date, patches stop.
Alternatives
Ride the release train. Target the newest minor. Highest security posture and highest test burden, suited to platform teams with canary clusters and strong e2e coverage.
Stay one minor behind. The most common enterprise choice. Buys a few months of other people finding the bugs while staying comfortably inside support.
Vendor extended support. EKS, GKE, and AKS all sell paid support past upstream EOL. This buys calendar time, not safety, and costs real money. Use it to recover from a slip, never as a plan.
LTS-style distributions. Some vendors backport fixes into long-lived branches. That trades upstream velocity for stability and deepens vendor lock-in.
FAQs
Which minors are supported right now? 1.34, 1.35, and 1.36. 1.33 is end-of-life and receives no further patches.
Can kubelets be older than the control plane? Yes, and they must never be newer. Keep them within one minor as policy even though upstream allows more.
How long is a minor supported? Roughly fourteen months from release, covering the three-minor window plus a maintenance tail. Track the actual date per release rather than the rule of thumb.
Does upgrading Kubernetes force a Docker Engine upgrade? No. Docker Engine builds images in CI; containerd runs pods on nodes via CRI. The two version streams are independent.
Should we jump straight from 1.33 to 1.36? No. Go one minor at a time. Skipping minors is unsupported and skips the deprecation warnings that would have told you what breaks.
Who owns the upgrade calendar? The platform team owns the calendar and the gate. Application teams own fixing their own deprecated APIs before the window opens.
Related
- Why Governance Scales Where Heroics Don't
- API Deprecation Tracking
- Extension & Add-On Governance
- Governance Basics
- Governance Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).