How to Upgrade an EKS Cluster to 1.37 Safely: Step by Step (2026)
This guide is for engineers who own Amazon EKS clusters running managed node groups or Karpenter and need to move them up a Kubernetes minor version without an outage. By the end you will have a repeatable runbook: refresh EKS upgrade insights, scan for removed APIs, run pre-flight checks, upgrade the control plane, then the add-ons, then the nodes, and you will know what the 7-day rollback can and can't undo. The worked example goes from 1.36 to 1.37, which landed on EKS on 1 October 2026. Commands match the EKS user guide and API reference as they read this week; the current tool versions are AWS CLI v2.38.5, eksctl 0.231.0, Pluto 5.24.4 and Karpenter 1.15.1. You need kubectl with cluster-admin, IAM permissions on the cluster, and ideally a staging cluster on the same version to rehearse on. If you want the background first (support windows, the $0.60 an hour extended support charge, Auto Mode), read the EKS in 2026 explainer.
Step 1: Check versions, support status and node skew
Start by writing down where you are. Set three variables that every later command uses:
export CLUSTER=prod-eu1
export AWS_REGION=eu-west-1 # the AWS CLI reads this as the default region
export TARGET=1.37 # exactly one minor above the current control plane
# Control plane version, platform version, upgrade policy and Auto Mode flag
aws eks describe-cluster --name "$CLUSTER" \
--query 'cluster.{version:version,platform:platformVersion,policy:upgradePolicy.supportType,autoMode:computeConfig.enabled}' \
--output table
# Support dates for the current and target versions
aws eks describe-cluster-versions --cluster-versions 1.36 1.37 \
--query 'clusterVersions[].{v:clusterVersion,status:versionStatus,endStd:endOfStandardSupportDate,endExt:endOfExtendedSupportDate}' \
--output table
# kubelet, OS image and container runtime on every node (Fargate pods show up as nodes too)
kubectl get nodes -o custom-columns=NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,OS:.status.nodeInfo.osImage,RUNTIME:.status.nodeInfo.containerRuntimeVersion
Three rules come out of this. An in-place upgrade moves one minor version at a time, so 1.34 to 1.37 is three separate passes. AWS asks that managed and Fargate nodes run the same minor as the control plane before each control plane upgrade, so every pass includes the nodes. The upstream skew policy lets a kubelet lag up to three minors behind the API server but never run ahead of it; treat that as headroom for stragglers such as self-managed nodes, not as a reason to stack control plane hops. And your kubectl must be within one minor of the cluster, so install a 1.37 client before you finish.
If the RUNTIME column still shows containerd://1.x or the OS column says Amazon Linux 2, fix that first. Kubernetes 1.35 was the last release to support containerd 1.x, and EKS has published no AL2 AMIs since 1.32, according to the EKS release notes.
Step 2: Refresh and read EKS upgrade insights
Cluster insights check every cluster daily for deprecated API usage, kubelet and kube-proxy skew, EKS add-on compatibility and cluster health. Refresh them now rather than trusting yesterday's run:
# On-demand refresh (API added August 2025); poll until status is COMPLETED
aws eks start-insights-refresh --cluster-name "$CLUSTER"
aws eks describe-insights-refresh --cluster-name "$CLUSTER" --query status --output text
# Everything not PASSING for upgrade readiness
aws eks list-insights --cluster-name "$CLUSTER" \
--filter '{"categories":["UPGRADE_READINESS"],"statuses":["ERROR","WARNING","UNKNOWN"]}' \
--query 'insights[].{id:id,name:name,version:kubernetesVersion,status:insightStatus.status}' \
--output table
# Affected resources, deprecated API details and the recommendation for one insight
aws eks describe-insight --cluster-name "$CLUSTER" --id a1b2c3d4-5678-90ab-cdef-EXAMPLE11111 \
--query 'insight.{recommendation:recommendation,resources:resources,deprecations:categorySpecificSummary.deprecationDetails}'
When the refresh is in progress, describe-insights-refresh returns IN_PROGRESS; it ends as COMPLETED or FAILED. The CLI must be recent: the 2.27.7 build on my laptop rejects start-insights-refresh with aws: error: argument operation: Invalid choice.
How to read the results: ERROR usually means the problem breaks on the next minor version (N+1), WARNING means N+2 or later, and UNKNOWN means EKS couldn't evaluate the check. Fix every ERROR before you go further. The deprecated-API insight looks at a rolling 30-day window of requests, so it can stay red for weeks after you fix a client. EKS briefly enforced insights by refusing upgrades unless you passed --force, then rolled that enforcement back in March 2025, and the docs still describe it as rolled back. The upgrade won't stop you, so the discipline has to come from you.
Step 3: Scan manifests and Helm releases for removed APIs
Insights only see what has hit the API server in the last 30 days. A CronJob that runs quarterly, or a chart you haven't deployed yet, is invisible to them, so scan the source as well. The EKS best practices guide notes that scanning static manifests is more accurate than scanning a live cluster.
# Pluto 5.24.4 (released 15 Sep 2026). Target version must start with "v".
pluto detect-files -d ./deploy --target-versions k8s=v1.37.0 -o wide
helm template ./charts/myapp | pluto detect - --target-versions k8s=v1.37.0 -o wide
pluto detect-helm --target-versions k8s=v1.37.0 -o wide
# Exit codes for CI: 2 = deprecated apiVersion found, 3 = removed apiVersion found
# What clients actually requested from the API server since it last restarted
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
Pluto's exit codes make it easy to fail a pipeline on removed APIs. I would not rely on kube-no-trouble (kubent) for current hops: its last tagged release is 0.7.3 from August 2024, and its rule files stop at 1.32.
For 1.36 and 1.37 the bigger risks are behaviour changes rather than API removals. The upstream deprecation guide lists nothing removed after 1.32, but EKS's notes flag several things worth a command each:
# 1.36: the kubelet refuses gitRepo volumes. List pods that still use one.
kubectl get pods -A -o json | jq -r '.items[] | select(any(.spec.volumes[]?; has("gitRepo"))) | "\(.metadata.namespace)/\(.metadata.name)"'
# 1.37: CSI drivers that opt in to SELinux mount labelling (SELinuxMount is on by default)
kubectl get csidriver -o custom-columns=NAME:.metadata.name,SELINUXMOUNT:.spec.seLinuxMount
# 1.37: dashboards and alerts that use cAdvisor metrics the kubelet no longer exports
grep -rnE 'container_cpu_load_average_10s|container_cpu_load_d_average_10s|container_tasks_state' ./dashboards ./alerts
1.36 also rejects new or updated objects with non-canonical IPs and CIDRs, such as 010.000.000.005 or 192.168.0.5/24, in built-in API kinds, so grep your Helm values for those too.
Step 4: Pre-flight checks: subnet IPs, PDBs, Karpenter and backups
The control plane upgrade needs up to five free IP addresses in the cluster's subnets and fails if a subnet is gone or full. Node drains fail if a PodDisruptionBudget allows zero disruptions. Check both:
# Free IPs in each cluster subnet (EKS needs up to 5 for new control plane ENIs)
aws ec2 describe-subnets --subnet-ids $(aws eks describe-cluster --name "$CLUSTER" \
--query 'cluster.resourcesVpcConfig.subnetIds' --output text) \
--query 'Subnets[*].[SubnetId,AvailabilityZone,AvailableIpAddressCount]' --output table
# PDBs that currently allow 0 disruptions will block node drains
kubectl get pdb -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,MIN:.spec.minAvailable,MAXUNAVAIL:.spec.maxUnavailable,ALLOWED:.status.disruptionsAllowed \
| awk 'NR==1 || $5=="0"'
A PDB with ALLOWED 0 on a single-replica deployment is the classic upgrade blocker. Scale the deployment to two, or relax the budget, before you touch nodes.
Upgrade cluster-level controllers before the control plane when their support depends on the Kubernetes version. Karpenter's compatibility matrix requires 1.15.x for Kubernetes 1.37 (and at least 1.13 for 1.36):
# Karpenter 1.15.1 (released 23 Oct 2026). Upgrade the CRD chart and controller together.
export KARPENTER_VERSION=1.15.1
export KARPENTER_NAMESPACE=kube-system
helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd \
--version "$KARPENTER_VERSION" --namespace "$KARPENTER_NAMESPACE"
helm upgrade karpenter oci://public.ecr.aws/karpenter/karpenter \
--version "$KARPENTER_VERSION" --namespace "$KARPENTER_NAMESPACE" \
--values karpenter-values.yaml --wait
Read the Karpenter 1.15 upgrade notes first: it rejects podsPerCore on Bottlerocket and multiple clusterDNS entries, and renames several metric label values. If you run the Cluster Autoscaler instead, its minor version must match the cluster. When I checked on 24 October, the newest tag on the autoscaler releases page was cluster-autoscaler-1.36.1 from 23 July, so a cluster that depends on it should wait for a 1.37 release. Finally, take a backup with AWS Backup or Velero: rollback restores the control plane version, not your data.
Step 5: Upgrade the control plane one minor version
Either tool works. The EKS docs set eksctl 0.215.0 as the minimum for this procedure; recent releases read the supported version list from the EKS API, and I would use the current 0.231.0 for a 1.37 target:
# Option A: eksctl
eksctl upgrade cluster --name "$CLUSTER" --version "$TARGET" --approve
# Option B: AWS CLI, then poll the update until it leaves InProgress
UPDATE_ID=$(aws eks update-cluster-version --name "$CLUSTER" \
--kubernetes-version "$TARGET" --query 'update.id' --output text)
until [ "$(aws eks describe-update --name "$CLUSTER" --update-id "$UPDATE_ID" \
--query 'update.status' --output text)" != "InProgress" ]; do sleep 30; done
aws eks describe-update --name "$CLUSTER" --update-id "$UPDATE_ID" \
--query 'update.{status:status,type:type,errors:errors}'
kubectl version # Server Version should now report v1.37.x
Expected final status is Successful. The upgrade takes several minutes and cannot be paused or stopped once started. EKS replaces API server instances behind the endpoint, so clients have to reconnect; current kubectl and official client libraries do this on their own, but long-lived custom controllers sometimes don't. If EKS's own health checks fail, it reverts the infrastructure and the cluster stays on 1.36. In Terraform, the equivalent of --force is force_update_version, supported by the AWS provider since v5.95.0 according to the roadmap issue above.
Step 6: Upgrade the EKS add-ons: VPC CNI, CoreDNS and kube-proxy
EKS does not update add-ons when you upgrade the control plane. First compare what is installed with the default version for the target:
for ADDON in vpc-cni coredns kube-proxy eks-pod-identity-agent aws-ebs-csi-driver; do
CURRENT=$(aws eks describe-addon --cluster-name "$CLUSTER" --addon-name "$ADDON" \
--query 'addon.addonVersion' --output text 2>/dev/null || echo "not-installed")
DEFAULT=$(aws eks describe-addon-versions --kubernetes-version "$TARGET" --addon-name "$ADDON" \
--query 'addons[0].addonVersions[?compatibilities[0].defaultVersion==`true`].addonVersion | [0]' \
--output text)
echo "$ADDON current=$CURRENT default-for-$TARGET=$DEFAULT"
done
The EKS docs list the latest versions for 1.37 as vpc-cni v1.23.2-eksbuild.6, coredns v1.14.7-eksbuild.19 and kube-proxy v1.37.0-eksbuild.29. Update them one at a time and wait for each:
aws eks update-addon --cluster-name "$CLUSTER" --addon-name vpc-cni \
--addon-version v1.23.2-eksbuild.6 --resolve-conflicts PRESERVE
aws eks wait addon-active --cluster-name "$CLUSTER" --addon-name vpc-cni
aws eks update-addon --cluster-name "$CLUSTER" --addon-name coredns \
--addon-version v1.14.7-eksbuild.19 --resolve-conflicts PRESERVE
aws eks wait addon-active --cluster-name "$CLUSTER" --addon-name coredns
aws eks update-addon --cluster-name "$CLUSTER" --addon-name kube-proxy \
--addon-version v1.37.0-eksbuild.29 --resolve-conflicts PRESERVE
aws eks wait addon-active --cluster-name "$CLUSTER" --addon-name kube-proxy
PRESERVE keeps your custom values; OVERWRITE resets fields to EKS defaults; NONE changes nothing and fails on conflict. Two traps from the add-on update docs and the CoreDNS page: EKS add-ons move one minor version at a time, so a VPC CNI on 1.20 needs several hops, and the CoreDNS add-on owns its Corefile, so edits made directly in the ConfigMap disappear on update unless they live in the add-on's corefile configuration value. If DNS misbehaves afterwards, my CoreDNS troubleshooting guide walks through it.
Step 7: Upgrade the nodes: managed node groups, Karpenter, Auto Mode, Fargate
Managed node groups. Set how many nodes can be replaced at once, then move the group to the latest AMI release for 1.37:
# DEFAULT launches new nodes before draining old ones; MINIMAL does the reverse (useful for scarce GPUs)
aws eks update-nodegroup-config --cluster-name "$CLUSTER" --nodegroup-name system \
--update-config maxUnavailablePercentage=20,updateStrategy=DEFAULT
NG_UPDATE=$(aws eks update-nodegroup-version --cluster-name "$CLUSTER" --nodegroup-name system \
--kubernetes-version "$TARGET" --query 'update.id' --output text)
until [ "$(aws eks describe-update --name "$CLUSTER" --nodegroup-name system --update-id "$NG_UPDATE" \
--query 'update.status' --output text)" != "InProgress" ]; do sleep 60; done
# eksctl equivalent
eksctl upgrade nodegroup --name=system --cluster="$CLUSTER" --kubernetes-version="$TARGET"
Use one tool or the other per node group, and let each update finish before starting the next. Each phase has hard limits: a new node must join within 15 minutes, or the update fails with NodeCreationFailure; pods must leave an old node within 15 minutes, or it fails with PodEvictionFailure. maxUnavailable tops out at 100 nodes. Node groups whose launch template pins a custom AMI can't be moved by version number: build a new AMI, create a launch template version, and pass --launch-template name=...,version=... instead.
Karpenter. After the control plane upgrade, Karpenter's drift detection notices that nodes run AMIs for the previous version and replaces them, as the EKS best practices guide describes. Drift is enabled by default in Karpenter v1; you control its pace with disruption budgets:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: KarpenterNodeRole-prod-eu1 # your node IAM role
amiSelectorTerms:
- alias: al2023@v20261019 # pinned EKS AMI release; avoid @latest in production
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: prod-eu1
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: prod-eu1
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
budgets:
- nodes: "10%" # replace at most 10% of nodes at a time for drift
reasons: ["Drifted"]
- nodes: "0" # no drift replacements 08:00 to 18:00 UTC on weekdays
schedule: "0 8 * * mon-fri"
duration: 10h
reasons: ["Drifted"]
The alias pins an AMI release date, and the AMIs behind it are published per Kubernetes version (the Karpenter AMI docs look them up by version in SSM). v20261019 is the current EKS AMI release, built for every version from 1.31 to 1.37. Watch progress with kubectl get nodeclaims and kubectl get nodes -L karpenter.sh/nodepool. Pods annotated karpenter.sh/do-not-disrupt hold their nodes back, which is the usual reason a few old nodes linger.
Auto Mode needs nothing: it starts replacing nodes after the control plane upgrade and respects PDBs. Fargate pods keep their old kubelet until recreated, so run kubectl rollout restart deployment/NAME -n NAMESPACE for each Fargate workload. Self-managed nodes need a new AMI, which you can look up from /aws/service/eks/optimized-ami/1.37/amazon-linux-2023/x86_64/standard/recommended/image_id in SSM.
Step 8: Verify, then decide within 7 days whether to roll back
# Every node on v1.37, nothing stuck outside Running or Succeeded
kubectl get nodes -o custom-columns=NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion
kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded
# Rollback readiness insights exist only during the 7 days after an upgrade
aws eks list-insights --cluster-name "$CLUSTER" --filter '{"categories":["ROLLBACK_READINESS"]}' \
--query 'insights[].{name:name,status:insightStatus.status}' --output table
Since July 2026 EKS can roll the control plane back one minor version within 7 days of an in-place upgrade, keeping etcd data and workloads. The order is the upgrade in reverse:
# 1. Node groups first: nodes may never be newer than the control plane
aws eks update-nodegroup-version --cluster-name "$CLUSTER" --nodegroup-name system --kubernetes-version 1.36
# 2. Downgrade any add-on that the rollback insights flag as incompatible with 1.36 (update-addon --addon-version ...)
# 3. Then the control plane. --force skips insight checks only, never the 7-day or one-version rules.
aws eks update-cluster-version --name "$CLUSTER" --kubernetes-version 1.36
EKS doesn't roll back add-ons, managed node groups, or self-managed and hybrid nodes (you move those yourself), and it doesn't support Fargate pods. Rollback is unavailable for clusters created at their current version and after the forced upgrade at the end of extended support. Rolling back into a version in extended support needs the upgrade policy set to EXTENDED and restarts the $0.60 an hour charge.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
aws: error: argument operation: Invalid choice on start-insights-refresh |
AWS CLI older than the August 2025 API (2.27 lacks it) | Upgrade AWS CLI v2 |
| Deprecated API insight stays ERROR after you fixed manifests | Check covers a rolling 30-day window of requests | Confirm with apiserver_requested_deprecated_apis, then proceed; it is not enforced |
update-cluster-version fails on subnets or IPs |
Fewer than 5 free IPs, or a cluster subnet was deleted | Free addresses, or update-cluster-config with new subnets in the same VPC and AZs |
Node group update fails with PodEvictionFailure |
Pods not evicted within 15 minutes: a PDB allows 0 disruptions, or a pod tolerates every taint | Fix the PDB or scale up; use --force only if you accept the eviction |
Node group update fails with NodeCreationFailure |
No capacity, EC2 quota, broken user data, or join took over 15 minutes | Add instance types, raise quota, test the launch template |
update-addon fails with a conflict |
Fields changed outside EKS with --resolve-conflicts NONE |
Use PRESERVE and move custom settings into --configuration-values |
| CoreDNS add-on update fails | Your own PDB clashes with the one the add-on ships | Delete your PDB or update with OVERWRITE after saving settings |
Pods stuck in ContainerCreating on SELinux nodes after 1.37 |
SELinuxMount default; pods share a volume with different labels |
Align seLinuxOptions.level, or set seLinuxChangePolicy: Recursive |
| Karpenter nodes not replaced after the upgrade | Drift budget at 0, do-not-disrupt pods, or Karpenter older than 1.15 |
Check kubectl get nodeclaims and budgets; upgrade Karpenter |
| Rollback request rejected | Over 7 days, nodes still on 1.37, cluster created at 1.37, or STANDARD policy for an extended target | Roll nodes back first, set policy to EXTENDED, otherwise build a new cluster |
Pods that go Pending while nodes are replaced are almost always PDBs, quotas or NodePool limits; the Pending pods guide covers each message.
FAQ
How long does an EKS cluster upgrade take?
The control plane takes several minutes. Nodes take longer and scale with node count, maxUnavailable, and how fast pods drain; each managed node gets 15 minutes to join and 15 minutes to drain before the update fails. Budget an afternoon for a mid-sized cluster the first time.
Can I skip Kubernetes versions when upgrading EKS?
Not in place. Each update-cluster-version call moves exactly one minor version, and add-ons also move one minor at a time. To jump several versions at once you need a blue/green migration to a new cluster.
Should I upgrade nodes or add-ons first?
The EKS best practices order is control plane, then add-ons and controllers, then nodes, and that is what this guide follows. Nodes must never be newer than the control plane, and EKS asks that they match it before the next control plane upgrade.
Does EKS upgrade add-ons automatically?
No. EKS add-ons and self-managed add-ons stay on their versions until you update them. The exception is Auto Mode, whose built-in networking, storage and load balancing components AWS upgrades for you.
What I check first in production is the boring part: the PDB list with zero allowed disruptions, the Karpenter or Cluster Autoscaler version against the target, and whether Pluto runs in CI so removed APIs never reach a cluster in the first place. Then I rehearse the exact commands on staging and keep the 7-day rollback as the last resort, not the plan. The upstream retirement of ingress-nginx this year is a reminder that not every upgrade risk is an API version; if you still run it, the Gateway API migration guide is worth doing before the next hop. Official references: Update existing cluster, cluster insights, version rollback, managed node group updates and Karpenter disruption.
Related: Fix Kubernetes CrashLoopBackOff, Fix ImagePullBackOff and ErrImagePull and Kubernetes Requests and Limits Explained.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
Amazon EKS in 2026: 1.37, $0.60 Extended Support and Auto Mode Explained
Amazon EKS in October 2026: Kubernetes 1.34 to 1.37 in standard support at $0.10 an hour, 1.31 to 1.33 in extended support at $0.60, what the Auto Mode fee buys, Pod Identity vs IRSA, Hybrid Nodes pricing, the new 7-day rollback, and when EKS is the right choice.
13 minOct 31, 2026How to Run Postgres on Kubernetes with CloudNativePG 1.30 (2026)
Step-by-step CloudNativePG 1.30.1 guide: install the operator, create a three-instance PostgreSQL 18 cluster, back up to S3 with the Barman Cloud plugin, restore to a point in time, add PgBouncer, monitor with Prometheus, upgrade majors and fix common errors.
15 minOct 31, 2026How to Troubleshoot DNS with dig: NXDOMAIN, SERVFAIL, DNSSEC (2026)
Step-by-step DNS troubleshooting with dig and nslookup: compare your OS resolver, read dig output, tell NXDOMAIN from NODATA and SERVFAIL, query authoritative servers, use +trace, debug DNSSEC with +cd, EDE codes and a validator, negative TTLs and CNAME-at-apex errors.
17 min