The EKS upgrade playbook: zero-downtime version bumps
How to upgrade an Amazon EKS cluster to a new Kubernetes minor version without downtime. Exact order, pre-flight checks, rollback rules, and common failures.
Amazon EKS supports each Kubernetes minor version for 26 months — 14 months of standard support, then 12 months of extended support (billed at an additional per-cluster-hour rate). Extended-support versions are still functional, but the meter is running and the auto-upgrade at the end of the 26-month window will happen on AWS’s schedule, not the team’s. Upgrading proactively is the only sensible option.
Every EKS upgrade fails the same way when it fails: someone upgrades the control plane, forgets that node groups don’t move with it, and the cluster spends a week half-upgraded until a pod hits an API server that no longer speaks its version. This is the sequence that avoids that.
Which versions are actually supported right now
As of late 2026, EKS supports these Kubernetes minor versions:
- Standard support (no extra charge): 1.36 (latest, released June 2, 2026), 1.35, 1.34
- Extended support (additional cost per cluster-hour): 1.33, 1.32, 1.31
If you’re on 1.34 or 1.35, the upgrade you should be running right now is to 1.36. If you’re on anything in extended support, you’re already paying the surcharge — get out of it. Standard support ends 14 months after a version’s EKS release date; extended support adds another 12 months. After that 26-month window, EKS auto-upgrades the control plane on AWS’s timeline — nodes and add-ons are still the operator’s responsibility.
To see current support status per cluster:
aws eks describe-cluster --name my-cluster --region ap-south-1 \
--query 'cluster.{Version:version,Platform:platformVersion,Status:status}'
To see the full version lifecycle for planning:
aws eks describe-cluster-versions
The response includes endOfStandardSupportDate and endOfExtendedSupportDate per version — the two dates that should drive the upgrade calendar.
The upgrade order that saves you
EKS upgrades happen in a strict sequence. Each step assumes the previous one succeeded and gave the cluster time to stabilise.
- Pre-flight checks — deprecated APIs, add-on compatibility, node headroom
- Control plane — AWS-managed, one minor version bump per upgrade
- Managed node groups — one at a time, blue/green style
- Self-managed node groups — same principle, more manual
- Cluster add-ons — VPC CNI, CoreDNS, kube-proxy in that order
- Workload validation — smoke tests, dashboards, alerts
Skipping any step doesn’t crash the cluster immediately. It just embeds an incident to happen later.
Version-skip rule: N+1 only
EKS does not allow skipping minor versions on the control plane. Going from 1.34 to 1.36 requires stopping at 1.35 first — one minor version per upgrade, always. A cluster stuck four versions behind means four separate upgrade cycles, with validation time between each.
Node groups have more slack: nodes can run up to three minor versions behind the control plane per Kubernetes’s official version-skew policy. So a 1.36 control plane can technically drive 1.33 nodes for a short window. That flexibility exists so upgrades can be sequenced, not so nodes can stay behind indefinitely — anything more than one minor version of lag on nodes is technical debt accruing interest.
Pre-flight checklist — 45 minutes, done twice
Run this list once in the week before the upgrade window and once again the day of. Deprecations move between kubectl versions, so a check that passed Monday might fail Friday.
1. Check for deprecated API usage
Every Kubernetes minor version removes a few beta APIs. Manifests that use removed APIs stop working the moment the control plane is upgraded.
Install kubent, which scans a live cluster for deprecated API calls:
# macOS: brew install kubent
# Linux: curl -sSL https://raw.githubusercontent.com/doitintl/kube-no-trouble/master/scripts/install.sh | sh
kubent --target-version=1.36
Or with pluto (an alternative from Fairwinds — scans YAML manifests or Helm chart directories on disk):
# Install: brew install pluto (or download binary from github.com/FairwindsOps/pluto/releases)
pluto detect-files -d ./manifests --target-versions=k8s=v1.36.0
Every hit must be fixed before the upgrade — typically by updating a Helm chart to a newer version or by patching manifests to use the current API.
2. Confirm add-on versions are compatible
AWS publishes a compatibility matrix for each managed add-on per Kubernetes version. The three that always need attention:
aws eks describe-addon-versions --kubernetes-version 1.36 \
--addon-name vpc-cni
aws eks describe-addon-versions --kubernetes-version 1.36 \
--addon-name coredns
aws eks describe-addon-versions --kubernetes-version 1.36 \
--addon-name kube-proxy
Note the defaultVersion: true entry for each. Those are the versions to move to.
3. Verify sufficient node capacity for surge
Control plane upgrades don’t need node capacity — AWS handles them. Node group upgrades do. Rolling a managed node group replaces nodes one at a time (default) or in surge groups. Pods must be re-scheduled somewhere while old nodes drain.
Rule of thumb: keep at least 20% headroom on cluster CPU/memory during the upgrade window, or set maxUnavailable: 1 and let it take longer.
kubectl top nodes
kubectl describe nodes | grep -A3 "Allocated resources"
4. Snapshot PDB (PodDisruptionBudget) state
A PDB with minAvailable set too aggressively can block a drain forever. Audit:
kubectl get pdb --all-namespaces \
-o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,ALLOWED:.status.disruptionsAllowed
Any PDB showing ALLOWED: 0 will stop node drain. Either scale up the workload temporarily or relax the PDB during the upgrade window.
5. Test in a non-production cluster first
If the target version has never touched a non-production copy of the workloads, this is a red flag. Upgrade a staging cluster to the target version, run integration tests, then upgrade production.
Step 1 — upgrade the control plane
Once pre-flight is clean:
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version 1.36 \
--region ap-south-1
This takes 20–40 minutes. AWS updates the control plane in place — no downtime for workloads, but API server requests may briefly fail during the switch. kubectl retries usually paper over this transparently.
Watch progress:
aws eks describe-update \
--name my-cluster \
--update-id <update-id-from-previous-command> \
--region ap-south-1
status moves from InProgress → Successful (or Failed, in which case AWS auto-rolls-back).
Step 2 — upgrade managed node groups
Managed node groups do NOT auto-upgrade with the control plane. This is the step teams forget.
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name app-nodes \
--region ap-south-1
Omitting --kubernetes-version here upgrades the nodes to match the control plane’s version, which is almost always what’s wanted.
The update rolls one node at a time by default. To do it faster with more concurrent replacements (only safe when workload PDBs allow it):
aws eks update-nodegroup-config \
--cluster-name my-cluster \
--nodegroup-name app-nodes \
--update-config maxUnavailablePercentage=25
Behind the scenes, EKS cordons the old node, drains it (respecting PDBs), waits for pods to reschedule, then terminates. If drain fails on any pod, the whole update pauses.
Repeat this step for every node group. Do them in sequence, not in parallel, unless the cluster has serious over-provisioning.
Step 3 — upgrade the three critical add-ons
Order matters — VPC CNI first (pod networking), then CoreDNS (in-cluster DNS), then kube-proxy (service routing). Doing them out of order can leave pods without networking during the transition.
VPC CNI
aws eks update-addon \
--cluster-name my-cluster \
--addon-name vpc-cni \
--addon-version v1.19.0-eksbuild.1 \
--resolve-conflicts PRESERVE \
--region ap-south-1
Use --resolve-conflicts PRESERVE to keep any custom ConfigMap values that were set (e.g. WARM_ENI_TARGET). Without this flag, custom config gets overwritten with defaults.
Substitute the actual current version from describe-addon-versions output — the value above is illustrative.
CoreDNS
aws eks update-addon \
--cluster-name my-cluster \
--addon-name coredns \
--addon-version <version-from-describe-addon-versions> \
--resolve-conflicts PRESERVE \
--region ap-south-1
DNS interruptions during this upgrade are usually invisible (CoreDNS runs multiple replicas by default) but for a small cluster running one replica, expect 5–10s of intermittent DNS resolution failures.
kube-proxy
aws eks update-addon \
--cluster-name my-cluster \
--addon-name kube-proxy \
--addon-version <version-from-describe-addon-versions> \
--resolve-conflicts PRESERVE \
--region ap-south-1
kube-proxy runs as a DaemonSet — one pod per node. It restarts pod-by-pod during the addon update. Service traffic keeps flowing.
Step 4 — validate workloads
Once control plane, nodes, and add-ons are all on the new version:
# Cluster-wide sanity
kubectl get nodes # all Ready, all on the new version
kubectl version --short # client + server versions match target
kubectl get pods --all-namespaces | grep -v Running # anything not Running
# Recent events across the cluster
kubectl get events --all-namespaces --sort-by='.lastTimestamp' | tail -30
Run the app’s smoke test suite. Check dashboards (Prometheus, CloudWatch, Datadog) for the 30 minutes after the upgrade — new failure modes typically show up in that window.
Rollback rules — read before the upgrade, not after
EKS allows rollback of a control-plane upgrade only within 7 days of the upgrade completing. After 7 days, control-plane rollback is not supported — the path forward is either patch or a further upgrade. Node groups and add-ons rollback separately via explicit commands (aws eks update-nodegroup-version --kubernetes-version <old> and aws eks update-addon), not automatically with the control plane.
To roll back within the window (example: 1.36 → 1.35):
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version 1.35 \
--region ap-south-1
If rolling back to a version now in extended support, the cluster’s upgrade policy must be EXTENDED first:
aws eks update-cluster-config \
--name my-cluster \
--upgrade-policy supportType=EXTENDED \
--region ap-south-1
Clusters that were auto-upgraded at the end of extended support cannot be rolled back at all. This is why proactive upgrades on a schedule beat reactive ones under pressure.
Common failures and the fix for each
PodDisruptionBudget prevents node drain — update stuck
kubectl get pdb --all-namespaces shows a PDB with disruptionsAllowed: 0.
Fix: either scale the corresponding deployment up temporarily so the PDB can allow one disruption, or set maxUnavailable: 1 on the PDB (from minAvailable) for the upgrade window. Revert after.
API removed — workloads crash after control-plane upgrade
Skipped the kubent step. A workload was using autoscaling/v2beta2 HPA or another removed API. The API server returns 404 for those calls.
Fix: identify the workloads (kubectl logs on the crashing pods, or CI logs on failed deploys), update Helm charts or manifests to the current API version, redeploy.
VPC CNI update leaves pods without networking
Upgraded VPC CNI while nodes were rolling. Pods scheduled on newly rolled nodes come up but can’t reach anything.
Fix: never upgrade VPC CNI mid-node-roll. Complete the node group upgrade first, then upgrade the CNI. If already broken, kubectl rollout restart daemonset aws-node -n kube-system and confirm all aws-node pods reach Ready.
CoreDNS update breaks service DNS resolution
Upgraded CoreDNS to an incompatible version, or the Corefile custom config was overwritten.
Fix: check current CoreDNS config vs pre-upgrade backup. Restore custom Corefile blocks. If deep-broken, roll the add-on back:
aws eks update-addon --cluster-name my-cluster --addon-name coredns \
--addon-version <previous-known-good-version> \
--resolve-conflicts OVERWRITE --region ap-south-1
Managed node group update fails with “InsufficientCapacity”
The EC2 instance type used by the node group is out of stock in the AZ during the surge.
Fix: either switch the node group to a mixed instance type list, wait an hour and retry, or spread node groups across additional AZs.
Fargate pods stuck on old kubelet version after control-plane upgrade
Fargate nodes are one-pod-per-node. New pods get the new kubelet version. Existing pods keep the old one until they’re recycled.
Fix: rolling restart the Fargate-backed deployments after the control-plane upgrade completes:
kubectl rollout restart deployment/my-fargate-deployment -n my-namespace
After the upgrade — three habits worth building
- Document the upgrade in a runbook: which version, when, who, what broke, how it was fixed. Cluster history in a wiki page has saved more hours than any monitoring dashboard.
- Set a calendar reminder for the next standard-support end date. Upgrading before the extended-support billing kicks in is significantly cheaper.
- Automate the pre-flight checks in CI.
kubentandplutocan run against every PR that changes manifests — deprecated API usage never ships to production if the pipeline blocks it.
EKS upgrades never become fun, but they can become boring. Boring is the goal.
DevOps YAML Pack
40+ production-ready configs — Kubernetes, Docker, Terraform, Ansible, Helm, GitHub Actions. Every file commented. Copy, edit two lines, ship. MIT license, no watermarks.
Get the pack — ₹499 →