📋 Upgrade Fundamentals

Kubernetes follows a strict N±1 minor version skew policy. Upgrading one minor version at a time (e.g. 1.28 → 1.29) is the only supported path. Skipping versions (1.28 → 1.30) is unsupported and risks etcd schema incompatibilities and API breakage.

Version Skew Policy

ComponentAllowed skew vs kube-apiserverNotes
kube-apiserverN/A — upgraded firstAll other components are ±1 of the apiserver version
kube-controller-manager≤ N (same or one minor behind)Upgraded after apiserver on same node
kube-scheduler≤ NSame node as controller-manager
kubeletN-2 to N (2 minor behind is ok)Allows rolling worker upgrades over time
kube-proxyN-2 to NSame node as kubelet
kubectlN-1 to N+1Client can be one minor ahead or behind
🚨 Never upgrade more than one minor version at a time Kubernetes does not support skipping minor versions. Always upgrade 1.28→1.29, then 1.29→1.30. Each step requires a separate upgrade procedure. etcd data migrations and API removals between versions make skipping dangerous.

Upgrade Order

0
Pre-upgrade checks

Check deprecated APIs, take etcd snapshot, verify cluster health, read release notes for removed APIs

1
Upgrade etcd (if external)

One member at a time. If co-located, kubeadm handles this automatically

2
Upgrade first control-plane node

kubeadm upgrade apply — upgrades apiserver, controller-manager, scheduler, etcd (if stacked)

3
Upgrade remaining control-plane nodes

kubeadm upgrade node — one at a time, verify health between each

4
Upgrade worker nodes

Drain → upgrade packages → uncordon. Roll one node (or batch) at a time

5
Upgrade add-ons

CoreDNS, kube-proxy, CNI, metrics-server, cert-manager — check compatibility matrix

🔍 Pre-Upgrade Checks & Control-Plane Upgrade

Pre-Upgrade Checks

# 1. Check current cluster version
kubectl version --short
kubectl get nodes -o wide

# 2. Scan for deprecated/removed API usage
# Install pluto (Fairwinds)
pluto detect-all-in-cluster --target-versions k8s=v1.29.0
# COMPONENT          KIND         VERSION     DEPRECATED   REMOVED
# my-ingress         Ingress      networking.k8s.io/v1beta1  true    true  ← must fix!

# 3. Check all nodes are Ready and no pending evictions
kubectl get nodes
kubectl get pods -A --field-selector=status.phase=Pending

# 4. Verify etcd health
etcdctl endpoint health --cluster ...
etcdctl endpoint status --cluster --write-out=table

# 5. Take etcd snapshot BEFORE upgrading
etcdctl snapshot save /backup/pre-upgrade-$(date +%Y%m%d).db ...

# 6. Check available upgrade path
kubeadm upgrade plan
# COMPONENT                 CURRENT     TARGET
# kube-apiserver            v1.28.5     v1.29.3
# kube-controller-manager   v1.28.5     v1.29.3
# kube-scheduler            v1.28.5     v1.29.3
# etcd                      3.5.9       3.5.10
⚠️ Check removed APIs before upgrading Each minor release removes previously deprecated APIs. For example, 1.29 removed flowcontrol.apiserver.k8s.io/v1beta2. Any resources using removed APIs must be migrated before upgrading or the resource becomes inaccessible.

Upgrade First Control-Plane Node

# On the first control-plane node:

# 1. Upgrade kubeadm package
apt-mark unhold kubeadm
apt-get install -y kubeadm=1.29.3-1.1
apt-mark hold kubeadm

# 2. Verify new version
kubeadm version

# 3. Dry-run first to check for issues
kubeadm upgrade plan v1.29.3

# 4. Apply the upgrade
kubeadm upgrade apply v1.29.3
# [upgrade/successful] SUCCESS! Your cluster was upgraded to "v1.29.3".

# 5. Upgrade kubelet and kubectl on this node
apt-mark unhold kubelet kubectl
apt-get install -y kubelet=1.29.3-1.1 kubectl=1.29.3-1.1
apt-mark hold kubelet kubectl

# 6. Restart kubelet
systemctl daemon-reload
systemctl restart kubelet

# 7. Verify this control-plane node
kubectl get nodes
# NAME    STATUS   ROLES           VERSION
# cp-1    Ready    control-plane   v1.29.3   ← upgraded
# cp-2    Ready    control-plane   v1.28.5   ← not yet
# cp-3    Ready    control-plane   v1.28.5   ← not yet

Upgrade Additional Control-Plane Nodes

# On each additional control-plane node (cp-2, cp-3):
# Same package upgrade, but use 'kubeadm upgrade node' (not apply)

apt-mark unhold kubeadm && apt-get install -y kubeadm=1.29.3-1.1
kubeadm upgrade node          # ← different command for non-first CP nodes

apt-mark unhold kubelet kubectl
apt-get install -y kubelet=1.29.3-1.1 kubectl=1.29.3-1.1
systemctl daemon-reload && systemctl restart kubelet

# After all CP nodes: verify
kubectl get nodes
# NAME    STATUS   ROLES           VERSION
# cp-1    Ready    control-plane   v1.29.3
# cp-2    Ready    control-plane   v1.29.3
# cp-3    Ready    control-plane   v1.29.3
💡 Verify health between each node After upgrading each control-plane node, wait 60 seconds and run kubectl get nodes and etcdctl endpoint health before proceeding to the next. Don't rush — a broken second CP node while the third is still old can trigger quorum loss.

🔧 Worker Node Upgrade Strategies

In-Place Rolling

Drain, upgrade packages, restart kubelet, uncordon. One node (or batch) at a time. Requires capacity headroom for evicted pods.

Blue-Green Node Pool

Add new nodes (new version), cordon old nodes, let pods migrate naturally, then delete old nodes. Zero disruption, more expensive short-term.

Surge Upgrade

Add N new nodes before removing N old nodes. Used by managed Kubernetes (EKS managed node groups, GKE node pools). Cloud-provider automated.

Cluster Autoscaler Integration

Pause CA during upgrade to prevent it scaling old nodes back up. Resume after all old nodes are drained.

In-Place Worker Node Upgrade (kubeadm)

# For each worker node (repeat for each):

# 1. Cordon the node (stop new pods being scheduled here)
kubectl cordon worker-1

# 2. Drain (evict all pods gracefully)
kubectl drain worker-1 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --grace-period=60 \
  --timeout=300s

# Wait for drain to complete — check no pods remain except DaemonSets
kubectl get pods -o wide --field-selector spec.nodeName=worker-1

# 3. SSH to the node and upgrade packages
ssh worker-1
apt-mark unhold kubeadm kubelet kubectl
apt-get install -y \
  kubeadm=1.29.3-1.1 \
  kubelet=1.29.3-1.1 \
  kubectl=1.29.3-1.1
kubeadm upgrade node
systemctl daemon-reload
systemctl restart kubelet

# 4. Back on control plane — uncordon
kubectl uncordon worker-1

# 5. Verify node is Ready and new version
kubectl get node worker-1
# NAME       STATUS   ROLES    VERSION
# worker-1   Ready    <none>   v1.29.3

# 6. Repeat for next worker node

Blue-Green Node Pool (Cloud)

# AWS EKS / GKE / AKS approach:

# 1. Create new node group with target version
eksctl create nodegroup \
  --cluster my-cluster \
  --name workers-v129 \
  --kubernetes-version 1.29 \
  --nodes 5

# 2. Cordon all old nodes (prevent new scheduling)
kubectl cordon -l eks.amazonaws.com/nodegroup=workers-v128

# 3. Drain old nodes one at a time
for node in $(kubectl get nodes -l eks.amazonaws.com/nodegroup=workers-v128 -o name); do
  kubectl drain $node --ignore-daemonsets --delete-emptydir-data --force
done

# 4. Delete old node group after pods migrated
eksctl delete nodegroup --cluster my-cluster --name workers-v128
⚠️ PodDisruptionBudgets are honoured during drain kubectl drain respects PDBs — if a PDB prevents eviction, drain will block. Ensure PDBs allow at least one pod to be unavailable, or drain will time out. Use --disable-eviction only as a last resort in emergencies.

Handling Stuck Drains

# Check why drain is stuck
kubectl get pdb -A
kubectl get pods -o wide --field-selector spec.nodeName=worker-1

# PDB is blocking — check which PDB
kubectl describe pdb my-app-pdb
# Disruptions Allowed: 0  ← all replicas on one node, PDB prevents eviction

# Fix: scale up the deployment first so PDB allows disruption
kubectl scale deployment my-app --replicas=3

# Or temporarily patch the PDB (use with caution)
kubectl patch pdb my-app-pdb -p '{"spec":{"minAvailable":0}}'
# ... drain completes ...
kubectl patch pdb my-app-pdb -p '{"spec":{"minAvailable":1}}'

⏪ Rollback & Add-on Upgrades

Control-Plane Rollback

Kubernetes does not support downgrading the control plane via kubeadm. The only rollback path is restoring from the pre-upgrade etcd snapshot to a fresh cluster running the old version.

# Rollback procedure (last resort):
# 1. Stop kube-apiserver on all control-plane nodes
# 2. Restore etcd from pre-upgrade snapshot (see lesson 83)
# 3. Reinstall old kubeadm/kubelet/kubectl packages
# 4. Re-run kubeadm init with old version config

# This is why pre-upgrade etcd snapshots are MANDATORY.
# Without a snapshot, rollback is impossible.
🚨 Always snapshot etcd before upgrading Kubernetes explicitly states: downgrading is not supported. Your only safety net is an etcd snapshot. Take it immediately before running kubeadm upgrade apply. Store it off-cluster.

Add-on Compatibility Matrix

Add-onHow to upgradeCompatibility check
CoreDNSkubeadm upgrades automatically; or kubectl edit deployment coredns -n kube-systemCheck kubeadm release notes for bundled version
kube-proxykubeadm upgrades DaemonSet image automaticallyMust match cluster minor version
CNI (Cilium/Calico)helm upgrade cilium cilium/cilium --version X.Y.ZCheck CNI's Kubernetes version support matrix
metrics-serverhelm upgrade metrics-server metrics-server/metrics-serverK8s version in chart requirements
cert-managerhelm upgrade cert-manager jetstack/cert-managerCheck supported K8s versions in cert-manager docs
Ingress NGINXhelm upgrade ingress-nginx ingress-nginx/ingress-nginxCheck IngressClass API version compatibility
cluster-autoscalerImage tag must match cluster minor version exactlycluster-autoscaler:v1.29.x for K8s 1.29

Upgrade Automation with Managed Kubernetes

# EKS — upgrade control plane then node groups
aws eks update-cluster-version \
  --name my-cluster \
  --kubernetes-version 1.29

# Wait for control plane upgrade
aws eks wait cluster-active --name my-cluster

# Upgrade managed node group
aws eks update-nodegroup-version \
  --cluster-name my-cluster \
  --nodegroup-name workers \
  --kubernetes-version 1.29

# GKE — single command upgrades control plane + node pools
gcloud container clusters upgrade my-cluster \
  --master --cluster-version 1.29

gcloud container clusters upgrade my-cluster \
  --node-pool default-pool \
  --cluster-version 1.29

Upgrade in Non-Prod First

Always upgrade dev → staging → production. Test your workloads and add-ons against the new version before touching production.

Read Release Notes

Check the CHANGELOG for every minor version between current and target. Removed APIs and behaviour changes are listed explicitly.

Maintenance Window

Even "zero-downtime" upgrades cause brief API unavailability during etcd compaction and apiserver restart. Schedule during low-traffic periods.

Test Rollback Procedure

Practice restoring from an etcd snapshot in staging before you need to in production. A rollback you've never tested is not a rollback plan.

📝 Knowledge Check

Q1. Your cluster is running Kubernetes 1.27. You want to upgrade to 1.30. What is the correct approach?
  • A) Run kubeadm upgrade apply v1.30.0 directly — kubeadm handles multi-version jumps
  • B) Upgrade 1.27→1.28, then 1.28→1.29, then 1.29→1.30 — one minor version at a time
  • C) Upgrade workers first to 1.30, then upgrade control plane
  • D) Create a new 1.30 cluster and migrate workloads — in-place upgrade is not supported
B) One minor version at a time. Kubernetes only supports upgrading one minor version per step. Skipping minor versions is unsupported and risks etcd migration failures, removed API incompatibilities, and undefined controller behavior. Each hop (1.27→1.28, etc.) is a separate full upgrade cycle.
Q2. During kubectl drain worker-3, the command hangs for 10 minutes. What is the most common cause?
  • A) The node is running too many pods to drain quickly
  • B) A PodDisruptionBudget has minAvailable set such that no pods can be evicted
  • C) The kubelet on worker-3 is not responding
  • D) The cluster autoscaler is adding new nodes and blocking drain
B) PodDisruptionBudget blocking eviction. kubectl drain honours PDBs. If a PDB requires minAvailable: 1 and all replicas are on worker-3, no pod can be evicted. The fix is to scale up the deployment so replicas exist on other nodes before draining, or temporarily relax the PDB.
Q3. After upgrading the control plane to 1.29, you discover a critical bug. What is the correct rollback procedure?
  • A) Run kubeadm upgrade apply v1.28.x to downgrade
  • B) Roll back the kubelet packages on each node independently
  • C) Restore the pre-upgrade etcd snapshot to a cluster reinstalled with 1.28 binaries
  • D) Delete and recreate the control-plane nodes with the old version
C) Restore etcd snapshot. kubeadm does not support downgrading. The only supported rollback is: stop the API server, restore the pre-upgrade etcd snapshot, reinstall the old kubeadm/kubelet/kubectl packages, and reinitialise the control plane. This is why an etcd snapshot taken immediately before upgrading is non-negotiable.