Assumes you're comfortable with kubeadm bootstrapping from Part 6, managed cluster patterns from Parts 5 and 7, PodDisruptionBudgets from Part 2, and etcd operations from Part 4 — this chapter is about safely moving a running cluster forward in time, not re-introducing any of those foundations.
Table of Contents#
- Why This Part Exists
- The Kubernetes Release Cadence and Support Window
- Version Skew Policy, Precisely
- The kubeadm Upgrade Workflow
- A Full Worked kubeadm Upgrade, Node by Node
- Managed Cluster Upgrades — EKS, GKE, AKS Patterns
- In-Place vs. Blue-Green Node Upgrades
- A Worked Blue-Green EKS Node Upgrade
- Finding and Fixing Deprecated APIs Before Upgrading
- Upgrading Add-ons: CNI, CSI Drivers, and Operators
- PodDisruptionBudgets and Drain Safety During Upgrades
- Rollback Strategy — What Can and Can't Be Undone
- etcd's Own Version Lifecycle
- etcd Backup Before Any Upgrade
- Node OS and Kernel Patching — a Separate Lifecycle
- Testing an Upgrade Before Production
- Fleet-Wide Upgrade Orchestration
- Automating the Upgrade Pipeline
- A Full Worked Scenario: Upgrading the
checkoutCluster End-to-End - Post-Upgrade Verification Checklist
- Part 15 CLI Cheat Sheet
- A Worked Numeric Example: Why the Extra Rigor Pays For Itself
- Quick Reference: Every Upgrade Decision in This Chapter
- A Note on Upgrade Communication
- Upgrade Cadence by Environment Tier
- Common Mistakes and Interview Traps
- Worked Practice Problems
- Summary and What's Next
Why This Part Exists#
Every earlier part in this series described a cluster at a single point in time — this closing chapter is about the one thing every cluster owner eventually has to do repeatedly for the entire life of the cluster: move it forward to a new Kubernetes version without turning that routine maintenance into an outage. A cluster that's never upgraded doesn't stay safely frozen — it ages out of its vendor's support window, accumulates unpatched CVEs, and eventually faces a much riskier multi-version jump instead of a series of small, well-practiced ones. Treating upgrades as routine, low-drama maintenance rather than a rare, high-stakes event is the mindset shift this entire chapter is built around.
The throughline system closes out here too: checkout, catalog, inventory, and recommendations,
running across however many nodes this series' worked examples have accumulated, all moving through one
real upgrade cycle together in this chapter's final worked scenario. Every system this series introduced —
the GPU-backed recommendation-model from Part 14, the multi-tenant guardrails from Part 13, the security
posture from Part 11 — has to survive that same upgrade cycle intact, which is exactly what makes this a
fitting closing chapter rather than an afterthought: an upgrade is the one operation that touches every
other part of this series at once.
The Kubernetes Release Cadence and Support Window#
Kubernetes ships new minor versions on a regular cadence, and each one is supported for a limited window — understanding this rhythm is what turns "upgrade eventually" into an actual, plannable maintenance schedule rather than a reactive scramble once a cluster falls out of support.
Important
The single highest-leverage lifecycle practice in this entire chapter is staying inside the supported version window at all times, upgrading one minor version at a time on a predictable cadence — every extra minor version a cluster falls behind compounds both the number of breaking API changes to review at once (next section) and the number of accumulated, unpatched security advisories a cluster is silently exposed to. Confirm your specific Kubernetes distribution's exact support window length (it varies by distribution and has changed over time) rather than assuming a fixed number — the discipline of upgrading regularly matters more than memorizing one specific duration. A quarterly or twice-yearly upgrade cadence, planned and scheduled well in advance, is a realistic target for most organizations — the goal is a predictable, low-drama recurring event, not an occasional high-stakes scramble. Building that cadence into a recurring calendar commitment, rather than an ad-hoc "we should get to this eventually" backlog item, is what actually makes it happen consistently in practice.
Version Skew Policy, Precisely#
The version skew policy defines exactly how far out of sync different cluster components are allowed to be during an upgrade — this is what makes a safe, phased upgrade possible at all, rather than requiring every component to jump versions in one atomic, all-or-nothing operation.
| Component pair | Maximum allowed skew |
|---|---|
kubelet vs. kube-apiserver | kubelet may be up to 3 minor versions older than the API server, never newer |
kube-apiserver instances (HA control plane) | Must be within 1 minor version of each other during a rolling control-plane upgrade |
kube-controller-manager/kube-scheduler/cloud-controller-manager vs. kube-apiserver | Must never be newer than the API server they talk to |
kubectl vs. kube-apiserver | Supported within 1 minor version in either direction |
This skew tolerance is precisely what makes the "control plane first, then workers" upgrade order (next section) both correct and safe — the API server can run one minor version ahead of the kubelets and still be within policy, giving an operator a real, supported window to roll worker nodes forward gradually rather than needing to upgrade every node in the cluster in one synchronized instant. Without this tolerance, every cluster upgrade would require a full, simultaneous stop-the-world cutover across every node at once — exactly the kind of high-risk, all-or-nothing operation the phased approach in this chapter exists to avoid.
Warning
Skipping a minor version during an upgrade (e.g. 1.29 straight to 1.31) is unsupported — kubeadm and the version skew policy both assume sequential, one-minor-version-at-a-time upgrades. A cluster that has fallen multiple versions behind cannot shortcut back to current in one step; it must be upgraded through each intermediate minor version in sequence, which is precisely why the earlier "stay inside the support window" guidance compounds in cost the longer it's deferred. A cluster three minor versions behind doesn't face one upgrade — it faces three, each with its own deprecated-API review and verification pass.
The kubeadm Upgrade Workflow#
Part 6 covered kubeadm bootstrapping a cluster from scratch — upgrading an existing kubeadm cluster follows a distinct, ordered workflow that respects the version skew policy above at every step.
| Step | Command | Why this order |
|---|---|---|
| 1 | kubeadm upgrade plan | Shows exactly what will change and confirms the target version is a valid, sequential step — never skip this dry-run |
| 2 | kubeadm upgrade apply <version> on the primary control plane node | Only the first control plane node uses apply; it performs the actual cluster-wide upgrade coordination |
| 3 | Upgrade kubelet/kubectl on that same node | The node components must catch up to what the control plane node just became |
| 4 | kubeadm upgrade node on every additional control plane node | Subsequent control plane nodes join the already-upgraded state, they don't re-run apply |
| 5 | kubeadm upgrade node on each worker, one at a time, always draining first | Workers can safely lag per skew policy — upgrading them gradually, not all at once, keeps the cluster serving traffic throughout |
A Full Worked kubeadm Upgrade, Node by Node#
# On the primary control plane node
kubeadm upgrade plan
apt-get update && apt-get install -y kubeadm=1.31.0-1.1
kubeadm upgrade apply v1.31.0
apt-get install -y kubelet=1.31.0-1.1 kubectl=1.31.0-1.1
systemctl daemon-reload && systemctl restart kubelet
# On each ADDITIONAL control plane node
apt-get install -y kubeadm=1.31.0-1.1
kubeadm upgrade node
apt-get install -y kubelet=1.31.0-1.1 kubectl=1.31.0-1.1
systemctl daemon-reload && systemctl restart kubelet
# On each worker node, ONE AT A TIME
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
apt-get install -y kubeadm=1.31.0-1.1
kubeadm upgrade node
apt-get install -y kubelet=1.31.0-1.1 kubectl=1.31.0-1.1
systemctl daemon-reload && systemctl restart kubelet
kubectl uncordon <node-name>--ignore-daemonsets --delete-emptydir-data on the drain step is worth explaining precisely, since both
flags silently change what actually happens: DaemonSet-managed pods (Part 2) are excluded from drain
because they're expected to run on every node by design and will simply restart on the node once it's
back — draining would otherwise fail waiting for something that isn't supposed to leave. --delete-emptydir-data
acknowledges that any pod using an emptyDir volume (Part 3) will lose that data on eviction, since
emptyDir is explicitly node-local, ephemeral storage — omitting this flag causes drain to refuse to
proceed at all rather than silently destroying data.
Managed Cluster Upgrades — EKS, GKE, AKS Patterns#
Parts 5 and 7 covered managed Kubernetes generally — the upgrade-specific pattern across all three major providers splits into two genuinely separate operations: control plane upgrade (fully provider-managed) and node group/pool upgrade (where the operator retains real, consequential choices).
| Provider | Control plane upgrade | Node upgrade strategies available |
|---|---|---|
| EKS | Provider-managed, one-command trigger | Managed node group in-place rolling replacement, or a separate blue/green node group swap |
| GKE | Provider-managed; Autopilot mode can also auto-upgrade automatically on a release channel | Surge upgrade (rolling, with configurable extra-node surge), or blue-green node pool upgrade |
| AKS | Provider-managed | Rolling node image upgrade, or blue-green node pool upgrade (preview-stage as of this series' research — confirm current availability before depending on it) |
The version skew policy from earlier in this chapter applies identically here even though the control plane upgrade itself is a "black box" — the moment the control plane finishes its provider-managed upgrade, every existing node's kubelet is now potentially several minor versions behind it, and the same "is this within the allowed 3-minor-version skew" check applies just as strictly as it would on a self-managed kubeadm cluster.
In-Place vs. Blue-Green Node Upgrades#
Every managed provider's node upgrade ultimately chooses between two fundamentally different strategies — understanding the tradeoff, not just the button to click, is what separates an operator who can debug a failed upgrade from one who's just following a wizard.
| In-Place Rolling | Blue-Green | |
|---|---|---|
| Rollback | Roll forward with a fix, or replace nodes again — there's no "old node group" still sitting there to fall back to | Simply don't cut traffic over, or shift back — the old node group is still fully intact and running |
| Cost during upgrade | Lower — no duplicate node capacity | Higher — briefly running two full node groups |
| Validation window | Limited — each replaced node immediately starts taking production traffic | Full — the new node group can be validated (synthetic traffic, canary workloads) before any real cutover |
| Best fit | Routine, low-risk upgrades on a well-tested version bump | A major version jump, a new node AMI/image with significant changes, or any upgrade where instant rollback matters more than cost |
A Worked Blue-Green EKS Node Upgrade#
# 1. Create a new, target-version managed node group alongside the existing one
eksctl create nodegroup --cluster checkout-prod --name workers-v1-31 \
--node-type m5.xlarge --nodes 5 --node-version 1.31
# 2. Cordon the OLD node group so no new pods schedule there
kubectl cordon -l eks.amazonaws.com/nodegroup=workers-v1-30
# 3. Drain old nodes gradually, letting the Scheduler place evicted
# pods onto the new, already-Ready node group
kubectl drain <old-node> --ignore-daemonsets --delete-emptydir-data
# 4. Once every old node is drained and empty, confirm application
# health, THEN remove the old node group
eksctl delete nodegroup --cluster checkout-prod --name workers-v1-30The rollback path here is genuinely simple precisely because both node groups coexist during the
transition: if step 3's drained pods show any problem on the new node group, kubectl uncordon on the
old node group immediately makes it schedulable again, and new pods (or a rolled-back Deployment) can land
back on the known-good version without any node ever having been destroyed — this is the direct,
concrete payoff of the "old node group is still fully intact" row from the comparison table above. Step 4's
old-node-group deletion should be treated as a deliberate, separately-approved action, not an automatic
tail end of the same script — it's the one point in this sequence that actually forecloses the rollback
path this whole strategy exists to preserve.
Finding and Fixing Deprecated APIs Before Upgrading#
Every Kubernetes upgrade can remove APIs that were deprecated long enough ago — an apply that worked
perfectly on the old version can fail outright on the new one if it targets a apiVersion that no longer
exists, which is why checking for this before upgrading, not after, is non-negotiable.
pluto detect-helm -o wide
pluto detect-files -d ./k8s-manifests/
kubectl-convert -f old-manifest.yaml --output-version apps/v1| Tool | What it does |
|---|---|
| Pluto | Scans manifests, Helm releases, and even live cluster objects for APIs deprecated in, or removed by, a specific target Kubernetes version |
kubent (kube-no-trouble) | Scans a live cluster's actual running objects for deprecated APIs, complementing Pluto's static-manifest scanning |
kubectl convert (a plugin, not built into kubectl by default) | Automatically rewrites a manifest from an old apiVersion to a current one |
Important
A concrete, real example worth internalizing rather than treating this as hypothetical: the
ingress-nginx project (kubernetes/ingress-nginx) was officially retired in March 2026. Any cluster
still running Ingress objects backed by that controller needs an active migration plan — this series'
Parts 8 and 9 covered Gateway API specifically as Ingress's architectural successor, and this retirement
is exactly the kind of forcing function that turns "Gateway API migration" from a nice-to-have
modernization project into a genuinely time-boxed necessity for any cluster still depending on the
retired controller.
A concrete before/after makes kubectl convert's value obvious rather than abstract. A manifest
authored years ago against a since-removed apiVersion fails outright on a newer cluster:
$ kubectl apply -f old-ingress.yaml
error: unable to recognize "old-ingress.yaml": no matches for kind "Ingress" in version "extensions/v1beta1"
$ kubectl convert -f old-ingress.yaml --output-version networking.k8s.io/v1 > new-ingress.yaml
$ kubectl apply -f new-ingress.yaml
ingress.networking.k8s.io/checkout createdThis is a genuinely low-effort fix once the deprecated API is actually identified — the friction is
almost always in finding every affected manifest across a large repository or Helm chart set (Pluto/kubent's
actual job) rather than in performing the conversion itself, which is precisely why the scanning step
matters more than the conversion step in practice. A GitOps-managed cluster (the automation-cicd-gitops
tutorial series) has a real advantage here: every manifest lives in one versioned source of truth, making a
repository-wide Pluto scan comprehensive by construction, rather than needing to separately track down
manifests that might exist only as ad-hoc kubectl apply history with no durable record anywhere.
Alpha APIs can be removed with zero deprecation notice at all, beta APIs are guaranteed at least 9 months or 3 minor releases of deprecation notice before removal, per the Kubernetes API deprecation policy — this is precisely why depending on an alpha feature in production (Part 14's DRA caveat is a direct example) carries meaningfully more upgrade risk than depending on a stable, GA API.
Upgrading Add-ons: CNI, CSI Drivers, and Operators#
Everything in this chapter so far upgrades core Kubernetes itself — the CNI plugin (Part 3), CSI storage drivers (Part 3), the service mesh control plane (Part 4), and every Operator-managed dependency (Part 4) all have their own, entirely separate version lifecycles that a cluster-version upgrade does not touch automatically.
| Add-on | Own compatibility contract |
|---|---|
| CNI plugin | Published compatibility matrix against specific Kubernetes minor versions — check it before assuming an old CNI version works fine on a newly upgraded control plane |
| CSI drivers | Similarly versioned against Kubernetes API compatibility, particularly for newer CSI feature gates a fresh Kubernetes version might enable |
| Service mesh (Istio, Linkerd) | Its own independent release cadence and Kubernetes-version support matrix — a mesh control plane lagging too far behind can itself become the next upgrade's blocker |
| CRDs installed by any Operator | A CRD's schema can itself need updating for a new Kubernetes version's stricter validation, independent of the workload it manages |
Warning
Confirm every add-on's published compatibility matrix against the target Kubernetes version before upgrading the cluster itself, not just after something breaks — a CNI or CSI driver silently incompatible with a newly upgraded control plane's API changes can produce exactly the kind of confusing, hard-to-diagnose networking or storage failure Part 10's troubleshooting chapters were built to untangle, when the actual root cause was simply an add-on version nobody checked against the new cluster version.
PodDisruptionBudgets and Drain Safety During Upgrades#
Part 2 introduced PodDisruptionBudgets as a general concept — an upgrade is the single moment they matter
most, since kubectl drain (used throughout this chapter) explicitly respects them, refusing to evict a
pod if doing so would violate the budget.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: checkout-service-pdb
namespace: checkout
spec:
minAvailable: 2 # drain will never evict below 2 healthy replicas
selector:
matchLabels: { app: checkout-service }Caution
A PodDisruptionBudget set too strictly relative to actual replica count (e.g. minAvailable: 3 on a
Deployment that only ever runs 3 replicas) makes that workload permanently undrainable — every
upgrade, every node maintenance operation, will stall indefinitely on that pod, since evicting even one
replica would violate the budget with zero slack. Always size minAvailable/maxUnavailable with real
headroom below total replica count, and treat a drain that hangs indefinitely (Part 10's troubleshooting
instincts apply directly here) as a signal to check the target pod's PDB before assuming the node itself
is unhealthy.
Rollback Strategy — What Can and Can't Be Undone#
Not every part of a cluster upgrade is equally reversible — knowing which parts genuinely have a clean rollback path and which don't shapes how cautiously each step should be approached.
| Component | Rollback path |
|---|---|
| Node group (blue-green strategy) | Clean — the old node group still exists until deliberately deleted |
| Node group (in-place rolling) | No clean rollback — nodes have already been replaced; recovery means rolling forward with a fix, or rebuilding from the same old version's image |
| kubelet/kubeadm on a single node | Possible by reinstalling the prior package version, but genuinely fiddly and not officially a first-class supported path |
Control plane (kubeadm upgrade apply) | Not supported — kubeadm has no built-in control-plane downgrade path at all |
| etcd data | Restorable from a pre-upgrade snapshot (next section) — this is the actual safety net for a control-plane upgrade gone wrong, not a version rollback |
Caution
Because kubeadm has no supported control-plane downgrade path, the etcd snapshot taken immediately
before an upgrade (next section) is the real rollback mechanism for a self-managed control plane, not
"just downgrade the packages." A failed control-plane upgrade is recovered by restoring etcd to its
pre-upgrade state and rebuilding the control plane at the old version from that snapshot — a
meaningfully more involved recovery than a simple package downgrade, which is precisely why the next
section's backup step is never optional.
etcd's Own Version Lifecycle#
etcd (Part 1, Part 4) has its own independent release cadence and its own version compatibility rules — a Kubernetes minor version upgrade does not automatically upgrade the etcd binary underneath it, and kubeadm only bundles a specific, tested etcd version per Kubernetes release rather than always shipping etcd's own latest release.
kubectl -n kube-system get pods -l component=etcd -o jsonpath='{.items[0].spec.containers[0].image}'
ETCDCTL_API=3 etcdctl version| Consideration | Why it matters |
|---|---|
| kubeadm-managed etcd version is tied to the kubeadm release, not chosen independently | Upgrading Kubernetes via kubeadm typically upgrades etcd to whatever version that kubeadm release bundles — check the release notes rather than assuming etcd is untouched |
| Self-managed (non-kubeadm) etcd clusters need their own explicit upgrade plan | A cluster running etcd outside kubeadm's management (a common pattern in fully custom on-prem builds, Part 6) must track etcd's own compatibility matrix against the target Kubernetes version separately |
| etcd's own major-version compatibility (etcd 3.4 vs. 3.5, for instance) | A version jump here can involve its own distinct migration steps, independent of anything Kubernetes-specific |
Note
This is a genuinely easy detail to overlook precisely because kubeadm handles it transparently in the common case — a team running kubeadm rarely has to think about etcd's version explicitly at all. The risk surfaces specifically for self-managed etcd outside kubeadm's control, or when reading a kubeadm release's changelog to confirm exactly what changed before a major upgrade — "etcd was upgraded from 3.5.9 to 3.5.12 in this kubeadm release" is exactly the kind of line worth reading deliberately rather than skimming past.
etcd Backup Before Any Upgrade#
Part 4 covered etcd's disaster-recovery drill in general — a cluster upgrade is one of the specific, scheduled moments that backup exists for, and skipping it "just this once because the upgrade is routine" is exactly the shortcut that turns a routine upgrade into an unrecoverable incident.
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-pre-upgrade-$(date +%F).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-pre-upgrade-$(date +%F).db --write-out=tableThis single command, run and verified immediately before kubeadm upgrade apply, is the difference
between "a failed upgrade is a stressful but bounded recovery exercise" and "a failed upgrade is a
potential total cluster-state loss" — given the previous section's finding that kubeadm has no supported
downgrade path at all, this snapshot is not a nice-to-have companion to the upgrade, it's the only real
safety net a self-managed control plane upgrade has.
Node OS and Kernel Patching — a Separate Lifecycle#
A node's underlying operating system and kernel have their own patch cadence, entirely independent of the Kubernetes version running on top of them — a cluster can be perfectly current on Kubernetes while running nodes with months of unpatched OS-level CVEs, or vice versa.
| Layer | Typical patch trigger |
|---|---|
| Kubernetes version | This chapter's upgrade cadence — driven by the Kubernetes support window |
| Node OS/kernel | The distribution's own security-patch release cycle, often faster and more frequent than Kubernetes minor releases |
| Container runtime | Its own release cadence, occasionally forced by a specific CVE independent of both of the above |
For managed node images (EKS-optimized AMIs, GKE's Container-Optimized OS, AKS node images), a Kubernetes-version node upgrade and an OS-patch node upgrade are frequently the same operation — the managed image bundles a specific, tested combination of OS, kernel, container runtime, and kubelet together, so replacing a node onto a newer managed image (the blue-green or in-place patterns from earlier in this chapter) updates all three layers at once. Self-managed nodes (Part 6) don't get this bundling for free — OS patching there is a genuinely separate maintenance task from a Kubernetes version upgrade, and needs its own explicit schedule rather than being assumed to happen "whenever Kubernetes gets upgraded."
Tip
Even on managed node images where a Kubernetes upgrade and an OS patch often ship together, it's worth explicitly confirming this for a specific upgrade rather than assuming it — a managed provider can, and occasionally does, ship a Kubernetes-version-only node image update that doesn't include the latest available OS patches, particularly between scheduled AMI/image refresh cycles.
Testing an Upgrade Before Production#
Every technique in this chapter reduces risk during the upgrade itself — testing against a realistic staging cluster first reduces the chance of discovering a problem during production upgrade at all, shifting the moment of discovery to a much cheaper, lower-stakes environment.
| Staging fidelity gap | Risk it hides |
|---|---|
| Staging on an older Kubernetes version than production | The exact deprecated-API and add-on-compatibility risks this chapter covers go untested |
| Staging with different (usually lower) traffic/replica counts | PodDisruptionBudget sizing issues (too-strict minAvailable) may only manifest under production's real replica counts |
| Staging missing production's actual CRDs/Operators | An Operator's own upgrade-compatibility issue (previous section) never surfaces until it's already in production |
Tip
The highest-value staging fidelity investment for upgrade testing specifically is keeping staging's Kubernetes and add-on versions in lockstep with production at all times — not just spinning up a same-version staging cluster reactively right before a planned upgrade. A staging cluster that's already one or two versions behind production defeats the entire purpose of testing the next upgrade safely.
Fleet-Wide Upgrade Orchestration#
Part 13 introduced fleet-management tooling (Cluster API, Rancher Fleet) as the escape hatch for multi-tenancy at the separate-clusters tier — upgrade orchestration is exactly where that investment pays for itself directly, sequencing a version rollout across many clusters deliberately rather than leaving each cluster's upgrade to chance or manual tracking.
This is the direct multi-cluster analogue of the blue-green node-pool pattern from earlier in this chapter, one level up the stack — instead of validating a new node group before cutting production traffic to it, an organization running many clusters validates a new Kubernetes version on one canary cluster before rolling it to the rest of the fleet, catching a version-specific regression against one cluster's blast radius instead of discovering it simultaneously across every cluster the organization runs.
Automating the Upgrade Pipeline#
Every step this chapter has walked through manually — the deprecated-API scan, the etcd snapshot, the sequenced node upgrades, the post-upgrade verification — is mechanical enough to encode as a pipeline rather than a runbook a human executes by hand each time, and doing so is what makes the fleet-wide canary rollout from the previous section practically achievable at all.
The deprecated-API scan as an automated, run-blocking pipeline gate is the single highest-leverage
automation in this list — turning "someone remembered to run Pluto before upgrading" into "the pipeline
physically cannot proceed past a deprecated-API finding" removes the exact human-memory failure mode behind
a large fraction of real upgrade incidents, the same way a required CI test gate removes "someone forgot to
run the tests" as a failure mode for application deploys (the automation-cicd-gitops tutorial series
covers this general CI/CD gating pattern in depth, applied here specifically to cluster lifecycle rather
than application deployment).
A Full Worked Scenario: Upgrading the checkout Cluster End-to-End#
Every step in this final worked scenario draws directly from an earlier chapter in this series: the deprecated-API scan uses the Gateway API migration urgency from Parts 8-9, the etcd backup uses Part 4's disaster-recovery mechanics, the drain sequence respects the PodDisruptionBudgets from Part 2, and the whole sequence follows the version skew policy this chapter opened with — a fitting closing illustration that a safe production upgrade isn't a separate skill from everything else in this series, it's the disciplined application of all of it at once, under the added constraint of zero acceptable downtime. Any one of these seven steps skipped under time pressure reintroduces exactly the risk the rest of this chapter was written to eliminate.
Post-Upgrade Verification Checklist#
A cluster reporting every node Ready isn't sufficient proof an upgrade succeeded — this checklist
closes the gap between "the upgrade command completed" and "the cluster is genuinely healthy."
-
kubectl get nodes— every nodeReady, all reporting the expected new version underkubelet version -
kubectl get --raw='/readyz?verbose'(Part 10) — every control-plane health check passing, not just the top-line node status - Every namespace's critical workloads confirmed healthy — Ready count matches desired replicas, no
unexpected
CrashLoopBackOff/Pending(Part 10's diagnostic trees, run deliberately rather than waited-for) - Add-ons (CNI, CSI, service mesh) confirmed on a version compatible with the new cluster version, per the earlier section
-
pluto/kubentre-run against the now-upgraded cluster's live objects, confirming zero remaining deprecated-API usage - A synthetic end-to-end request through the throughline system's own critical path (a real request to
checkout-service, not just infrastructure-level health checks) confirmed successful - The pre-upgrade etcd snapshot retained per the organization's backup retention policy, not deleted immediately after a successful upgrade — a regression can surface days later, well after "the upgrade went fine" was declared
Part 15 CLI Cheat Sheet#
| Command | Purpose |
|---|---|
kubeadm upgrade plan | Dry-run showing exactly what an upgrade would change, before committing to it |
kubeadm upgrade apply <version> | Upgrades the primary control plane node — the only node that uses apply |
kubeadm upgrade node | Upgrades any additional control plane node, or a worker node |
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data | Safely evicts a node's workloads before upgrading it, respecting PodDisruptionBudgets |
pluto detect-files -d <dir> / kubent | Scan manifests or a live cluster for deprecated APIs before upgrading |
etcdctl snapshot save <path> | Take the pre-upgrade etcd backup — the real rollback mechanism for a self-managed control plane |
kubectl get --raw='/readyz?verbose' | Confirm full control-plane health after an upgrade, not just node status |
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion | Confirm every node actually reports the expected post-upgrade version |
A Worked Numeric Example: Why the Extra Rigor Pays For Itself#
The staging, PDB, and blue-green practices in this chapter all cost real engineering time — working through a concrete cost comparison makes clear why that investment is justified rather than taking it on faith.
Assume checkout-service processes $50,000 of transactions per hour at typical traffic, and a botched
in-place node upgrade (no PDB, no staging validation) causes a 45-minute partial outage before the team
identifies and rolls back the problem:
Direct revenue impact: 45 min × ($50,000/hour ÷ 60) ≈ $37,500
Incident response cost: 4 engineers × 3 hours × $150/hour (fully loaded) ≈ $1,800
Total direct cost of one incident: ≈ $39,300
Compare that to the ongoing cost of doing this chapter's recommended practices properly: a staging cluster kept in version lockstep (roughly one additional cluster's infrastructure cost, likely a few hundred dollars a month for a modest environment) plus perhaps 2-3 hours of engineering time per upgrade cycle to run the pre-upgrade deprecated-API scan and PDB review. A single avoided incident of the size above pays for years of that ongoing discipline — this is the same "prevention is cheaper than the incident it prevents" logic that justifies Part 11's staged security rollouts and Part 10's troubleshooting runbook investment, applied here specifically to upgrade risk.
Quick Reference: Every Upgrade Decision in This Chapter#
| Decision | Options | Covered in |
|---|---|---|
| How many versions to jump at once | Always exactly one minor version, sequentially — never skip | Version Skew Policy |
| Control plane upgrade order | Primary node (apply) first, then additional control plane nodes (upgrade node), then workers | The kubeadm Upgrade Workflow |
| Node upgrade strategy | In-place rolling (cheaper, no clean rollback) vs. blue-green (costlier, instant rollback) | In-Place vs. Blue-Green |
| Add-on versions | Must be independently confirmed compatible with the target Kubernetes version | Upgrading Add-ons |
| etcd version | Tracked separately from Kubernetes version, especially for self-managed etcd | etcd's Own Version Lifecycle |
| Node OS/kernel patching | A separate cadence from Kubernetes version, often bundled together only on managed node images | Node OS and Kernel Patching |
| Multi-cluster rollout | Canary cluster first, bake, then wave rollout — never simultaneous fleet-wide | Fleet-Wide Upgrade Orchestration |
| Rollback mechanism | Blue-green node groups: just don't cut over. Control plane: etcd snapshot restore, not a package downgrade | Rollback Strategy |
A Note on Upgrade Communication#
Every technique in this chapter reduces technical risk — the remaining risk in most real upgrades is organizational: stakeholders surprised by a maintenance window, an on-call engineer unaware an upgrade is in progress when an unrelated alert fires, or a dependent team's own deploy landing mid-upgrade. A technically perfect upgrade can still produce a confusing, wasted incident-response effort if nobody outside the upgrade team knew it was happening.
| Practice | Why it matters |
|---|---|
| Announce the maintenance window well ahead of time, even for a "routine" upgrade | A blue-green node upgrade's transient cost or a brief drain-induced latency blip can look like an incident to a team not expecting it |
| Include dependent/downstream teams in the announcement, not just the platform team's own channel | A team consuming checkout-service's API has no visibility into a platform-team-only announcement channel by default |
| Brief on-call before starting, not just the platform team running the upgrade | An alert firing mid-upgrade should be triaged with "is this upgrade-related" as the first question, not investigated from scratch |
| Share the rollback plan alongside the upgrade plan, before starting | Everyone involved should know what "abort and recover" looks like before they're deciding it under pressure, not discovering it mid-incident |
| Name a single accountable owner for the upgrade, even when several people are involved | Prevents the diffusion of responsibility that slows down a real decision during an in-progress incident |
| Freeze non-essential deploys during the upgrade window | Reduces the number of simultaneous changes if something does go wrong, keeping the upgrade itself as the only variable to investigate |
| Confirm the escalation path before starting, not mid-incident | Knowing who to page if the upgrade genuinely goes wrong should never be figured out for the first time under pressure |
| Keep the maintenance window's actual duration realistic, not optimistic | An upgrade window that runs long without warning erodes trust in future announced windows |
| Post a clear "upgrade complete, verification passed" signal afterward | Removes ambiguity about whether it's safe to resume normal deploy activity |
| Document the actual upgrade window in an incident/change log, even when nothing went wrong | Gives the next upgrade's planner a real historical baseline for how long this specific cluster's upgrade takes |
| Note any deviation from the planned procedure, however minor | A small ad-hoc workaround this time can become next time's forgotten landmine if it's never written down |
None of this is unique to Kubernetes — it's the same change-management discipline that applies to any production maintenance window — but it's worth stating explicitly here, since a technically flawless upgrade that blindsides an on-call engineer still counts as a bad outcome for the team involved, regardless of how cleanly every command in this chapter actually executed.
Upgrade Cadence by Environment Tier#
Not every cluster in a fleet should upgrade on the same schedule — a deliberate lag between environment tiers is itself a risk-reduction mechanism, turning staging into a live early-warning system for production rather than just a pre-deployment checklist.
| Tier | Typical lag behind a new release | Why |
|---|---|---|
| Dev/sandbox | Days | Lowest blast radius — the fastest place to discover an obvious regression |
| Staging | 1-2 weeks behind dev | Bakes under closer-to-production traffic patterns and data volume before touching real customers |
| Production | 2-4 weeks behind staging (never immediately upon release) | Absorbs the accumulated confidence from both earlier tiers; also gives the upstream community time to surface early patch releases for anything the GA release missed |
A cluster genuinely constrained to a single environment (no separate dev/staging) should still simulate this staggering — a scheduled, deliberate delay before touching production, rather than upgrading the moment a new version is available, preserves most of the benefit even without dedicated lower environments.
Tip
Best practice: never upgrade production on the very first patch of a new minor release
(X.Y.0) — wait for at least X.Y.1 or X.Y.2 unless a specific CVE forces an exception. A
surprising number of real-world regressions are found and patched within the first few weeks of a new
minor version's life, and a deliberately lagging production tier gets that fix bundled in automatically
instead of hitting the bug directly.
This staggered cadence is also what turns the fleet-wide canary rollout from earlier in this chapter into a two-layer safety net rather than a single check: staging catches a regression before it ever reaches a canary production cluster, and the canary cluster catches whatever staging's traffic pattern didn't happen to exercise.
Common Mistakes and Interview Traps#
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
| Skipping a minor version during upgrade (e.g. 1.29 → 1.31 directly) | Unsupported — kubeadm and the skew policy both assume sequential minor-version steps | Upgrade through every intermediate minor version in order |
Running kubeadm upgrade apply on more than one control plane node | Only the first control plane node uses apply; every additional one uses upgrade node | Follow the exact sequence — one apply, the rest upgrade node |
Draining a node without --delete-emptydir-data and assuming it will proceed | drain refuses to continue rather than silently discard emptyDir data | Explicitly acknowledge the data loss with the flag, after confirming it's actually acceptable |
| Treating a failed control-plane upgrade as downgradable via a package reinstall | kubeadm has no supported control-plane downgrade path at all | Restore etcd from the pre-upgrade snapshot and rebuild at the prior version instead |
| Sizing a PodDisruptionBudget with zero slack below replica count | Makes the workload permanently undrainable — every future upgrade stalls indefinitely on it | Always leave real headroom between minAvailable/maxUnavailable and total replica count |
| Skipping deprecated-API scanning "because the app hasn't changed" | The application not changing is irrelevant — the cluster's supported API surface changed under it | Always run Pluto/kubent against the target version before any upgrade, regardless of app-side changes |
| Assuming a CNI/CSI add-on works fine on a new Kubernetes version because nothing was touched | Add-ons have independent compatibility matrices from core Kubernetes — silently incompatible after a control-plane upgrade | Check each add-on's published compatibility matrix against the target version before upgrading |
| Upgrading every cluster in a fleet simultaneously to "get it over with" | Turns a contained, single-cluster regression into a simultaneous, fleet-wide outage | Canary one cluster first, bake, then roll out in waves |
| Assuming a Kubernetes-version node upgrade also patches the OS/kernel | True for many managed node images bundling both, but not guaranteed, and not true at all for self-managed nodes | Confirm OS/kernel patch status explicitly rather than assuming it rode along with the Kubernetes upgrade |
| Treating a manual, human-executed upgrade runbook as equivalent to an automated pipeline | Human memory is the weakest link in a repeated, high-stakes procedure — the exact failure mode automation exists to remove | Encode the deprecated-API scan and post-upgrade checklist as pipeline gates, not steps a person remembers to run |
| Deleting the pre-upgrade etcd snapshot immediately after declaring the upgrade successful | A regression can surface days later, well after the snapshot would have been useful for recovery | Retain pre-upgrade snapshots per the organization's normal backup retention policy, not a shorter ad-hoc window |
Worked Practice Problems#
Problem 1: A team upgrades their kubeadm cluster's control plane from 1.29 to 1.31 in one step, skipping 1.30 entirely, reasoning that "it's just two minor versions, it should be fine." The upgrade partially fails with confusing API-compatibility errors. What was the actual mistake?
Answer: Kubernetes and kubeadm's version skew policy explicitly does not support skipping a minor version during an upgrade — every upgrade path assumes sequential, one-minor-version-at-a-time steps, and jumping from 1.29 directly to 1.31 bypasses assumptions the upgrade tooling and the API server's own internal compatibility handling depend on. The correct path is upgrading 1.29 → 1.30 → 1.31 as two separate, sequential upgrade operations, verifying cluster health after each one before proceeding to the next.
Problem 2: After a successful control-plane upgrade via kubeadm upgrade apply, an operator notices
worker node kubelets are still on the previous minor version and asks whether this is a sign the upgrade
didn't fully complete. Is it?
Answer: No — this is expected and explicitly supported by the version skew policy, which allows kubelets to lag the API server by up to 3 minor versions. The control-plane-first, workers-gradually upgrade sequence in this chapter depends entirely on this tolerance; seeing older kubelet versions immediately after a control-plane upgrade is the correct intermediate state, not a failure, as long as the gap stays within the supported 3-minor-version window while workers are upgraded on their own rolling schedule.
Problem 3: A cluster owner wants the safest possible strategy for a major EKS node group upgrade involving a significantly changed node AMI, but is also under pressure to minimize infrastructure cost during the change. Which strategy should they choose, and what's the actual tradeoff they're accepting?
Answer: Blue-green node group upgrade is the safer choice for a significant AMI change specifically because it allows full validation of the new node group before any real traffic shifts onto it, and offers instant, clean rollback (the old node group remains fully intact) if something goes wrong — properties in-place rolling upgrade doesn't offer. The tradeoff being accepted is the transient cost of running two full node groups simultaneously during the validation and cutover window; the team is explicitly trading a bounded, temporary cost increase for meaningfully lower risk on a change significant enough (a new AMI, not just a routine version bump) to justify it.
Problem 4: A platform team runs 12 production clusters across different regions for different customer segments. They upgrade all 12 simultaneously over one weekend to "get it over with in one push." A version-specific regression in the new Kubernetes release affects every cluster identically at the same time. What upgrade-orchestration practice from this chapter would have limited the blast radius, and why didn't a per-cluster staging test (previous section) alone catch it?
Answer: A canary-then-wave fleet upgrade rollout (upgrade one cluster first, bake, then roll to the rest in waves) would have limited the blast radius to one cluster instead of all 12 simultaneously — a regression discovered on a single canary cluster is a contained incident; the same regression hitting all 12 clusters at once is a full outage across every customer segment simultaneously. A staging environment alone doesn't necessarily catch every production-scale regression, since staging's traffic patterns, data volume, or specific workload mix can differ enough from any single production cluster's real conditions — fleet-wide canary rollout adds a second, independent safety net by using a real production cluster as the first, contained exposure to the new version, rather than relying on staging fidelity alone.
Summary and What's Next#
Cluster upgrades are where every other discipline in this series converges at once — the version skew policy makes a phased rollout possible, PodDisruptionBudgets keep a drain safe, etcd backups are the real rollback mechanism for a self-managed control plane, and the deprecated-API discipline this chapter covered is exactly what Parts 8-9's Gateway API migration made concretely urgent. Treating an upgrade as routine, well-rehearsed maintenance — not a rare, high-stakes event — is the single idea worth carrying forward from this chapter above the rest.
Part 16 shifts from keeping a cluster current to sizing it correctly in the first place: capacity planning. Autoscaling (Part 12) reacts to load that has already arrived; Part 16 covers the slower-moving decisions — node pool shape, instance types, failure-domain headroom, growth forecasting — that determine the ceiling autoscaling reacts within. The two disciplines meet at exactly the moment this chapter's worked upgrade scenario needed extra node capacity to absorb a rolling drain — Part 16 is what makes sure that capacity was actually planned for, not discovered as a surprise mid-upgrade.