A step-by-step guide to safely upgrading an Amazon EKS cluster, including the control plane, managed node groups, self-managed nodes, and add-ons.
- Prerequisites
- Upgrade Strategy Overview
- Phase 1 — Pre-Upgrade Checks
- Phase 2 — Upgrade the Control Plane
- Phase 3 — Upgrade EKS Add-ons
- Phase 4 — Upgrade Managed Node Groups
- Phase 5 — Upgrade Self-Managed Node Groups
- Phase 6 — Post-Upgrade Validation
- Rollback Plan
- Troubleshooting
Before starting the upgrade, ensure you have the following in place:
- AWS CLI v2.x installed and configured https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
- kubectl matching the target Kubernetes version (±1 minor version)
- eksctl (latest version) — optional but recommended
aws --version
kubectl version --client
eksctl version
helm version- Install EKSCTL
curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin
- Create an EKS Cluster
eksctl create cluster --name my-cluster --version 1.34 --region ap-south-1 --nodegroup-name my-ng-1 --node-type t2.medium --nodes 2
⚠️ EKS supports upgrading only one minor version at a time.
For example:1.33 → 1.34 → 1.35. You cannot skip versions.
Upgrade order (always follow this sequence):
Control Plane → Add-ons → Node Groups → Validation
Skipping this order can result in incompatible API versions or broken cluster networking.
Check the Kubernetes changelog and AWS EKS release notes for deprecated APIs or breaking changes in the target version.
aws eks describe-cluster --name <cluster-name> --query "cluster.version" --output textUse kubectl to detect usage of deprecated APIs in your workloads:
kubectl get all --all-namespaces -o yaml | grep "apiVersionaws eks describe-addon-versions --kubernetes-version <target-version>aws eks describe-addon-versions --kubernetes-version 1.35 --addon-name coredns \
--query 'addons[].addonVersions[?addonVersion==`v1.12.4-eksbuild.10`].{Version:addonVersion,Compatibilities:compatibilities}'
Verify that all your installed add-ons (CoreDNS, kube-proxy, VPC CNI, etc.) have versions compatible with the target Kubernetes version.
# Export all namespace resources
kubectl get all --all-namespaces -o yaml > cluster-backup-$(date +%F).yaml
# Backup ConfigMaps and Secrets
kubectl get configmap,secret --all-namespaces -o yaml >> cluster-backup-$(date +%F).yamlBackup list for the production grade cluster:
- Kubernetes Resources (via Velero)
- All namespaces and namespace-scoped resources
- Deployments, StatefulSets, DaemonSets, ReplicaSets
- Services, Ingresses, NetworkPolicies
- ConfigMaps and Secrets
- PersistentVolumes and PersistentVolumeClaims
- RBAC — Roles, ClusterRoles, RoleBindings, ClusterRoleBindings
- ServiceAccounts and IRSA annotations
- HPA, VPA, PodDisruptionBudgets
- CRD definitions and all Custom Resource instances
- CronJobs, Jobs
- StorageClasses and VolumeSnapshotClasses
- AWS Infrastructure (via Terraform / IaC State)
- EKS cluster configuration (version, networking, logging settings)
- Node group configs (instance type, AMI ID, min/max/desired capacity)
- Launch Templates and their versions
- IAM Roles and Policies (especially IRSA roles)
- VPC, Subnets, Route Tables, Security Groups
- ALB/NLB listener rules, target groups, SSL certificates
- ECR image repositories and images
- EKS Specific (Manual Export)
aws-authConfigMap (IAM to RBAC mappings)- EKS managed add-on names and their exact versions (CoreDNS, kube-proxy, VPC CNI, EBS CSI)
- Cluster Autoscaler / Karpenter NodePool configurations
- Kubeconfig file
- Storage & Data (App-level)
- EBS volume snapshots for all attached volumes
- Database dumps —
pg_dump,mysqldump,mongodumpbefore upgrade - Redis RDB/AOF persistence backup
- Any in-cluster object storage data
- Helm & GitOps
- Helm release list with chart versions (
helm list -A) - Values for every Helm release (
helm get values) - ArgoCD Application CRs and project configs
- Flux Kustomization and HelmRelease objects
- Confirm Git repo is up to date (source of truth)
- Observability & Add-ons
- Prometheus rules and alerting configs
- Grafana dashboards (export as JSON)
- Fluentd/FluentBit ConfigMaps for log routing
- Cert-manager certificates and cluster issuers
- External-DNS configurations
Communicate a maintenance window to all relevant teams. Cluster upgrades may cause brief API server unavailability (typically < 5 minutes).
The control plane (API server, scheduler, controller manager) is managed by AWS and upgraded in-place with zero data plane disruption.
Via AWS CLI:
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version 1.35Via eksctl:
eksctl upgrade cluster --name my-cluster --region ap-south-1 --version 1.35 --approveVia AWS Console:
- Go to EKS → Clusters → <cluster-name>
- Click Update now next to the Kubernetes version
- Select the target version and confirm
aws eks describe-cluster --name my-cluster --query "cluster.status"
# Expected output: "UPDATING" → "ACTIVE"Or watch via eksctl:
eksctl utils describe-stacks --region ap-south-1 --cluster my-cluster⏱️ Control plane upgrades typically take 10–25 minutes.
kubectl version --short
# Server version should reflect the new versionAfter the control plane is upgraded, update the managed add-ons. Always upgrade add-ons before upgrading node groups.
aws eks list-addons --cluster-name my-clusteraws eks describe-addon-versions \
--addon-name <addon-name> \
--kubernetes-version <target-version> \
--query "addons[].addonVersions[].addonVersion"Upgrade the following add-ons in this order:
eksctl update addon \
--cluster my-cluster \
--name vpc-cni \
--version v1.21.1-eksbuild.7 \
--forceeksctl update addon \
--cluster my-cluster \
--name vpc-cni \
--version v1.14.2-eksbuild.4 \
--force eksctl update addon \
--cluster my-cluster \
--name vpc-cni \
--version v1.35.3-eksbuild.5 \
--force eksctl update addon \
--cluster my-cluster \
--name vpc-cni \
--version v0.8.1-eksbuild.6 \
--forceaws eks describe-addon \
--cluster-name <cluster-name> \
--addon-name <addon-name> \
--query "addon.status"
# Expected: "UPDATING" → "ACTIVE"Managed node groups support rolling updates with automatic cordon and drain behavior.
aws eks describe-nodegroup \
--cluster-name <cluster-name> \
--nodegroup-name <nodegroup-name> \
--query "nodegroup.releaseVersion"aws ssm get-parameter \
--name /aws/service/eks/optimized-ami/1.35/amazon-linux-2/recommended/release_version \
--query "Parameter.Value" \
--output textVia AWS CLI:
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name my-ng-1 \
--release-version <ami-release-version>Via eksctl:
eksctl upgrade nodegroup \
--name my-ng-1 \
--cluster my-cluster \
--kubernetes-version 1.35aws eks describe-nodegroup \
--cluster-name <cluster-name> \
--nodegroup-name <nodegroup-name> \
--query "nodegroup.status"
# Expected: "UPDATING" → "ACTIVE"Watch node rollout:
kubectl get nodes -w⏱️ Node group upgrades take 5–15 minutes per node, depending on cluster size.
For self-managed nodes (launched via Launch Templates or CloudFormation), you must manually cordon, drain, and replace nodes.
Prevent new pods from being scheduled on old nodes:
kubectl cordon <node-name>
# Or cordon all nodes in a group at once using a label
kubectl get nodes -l eks.amazonaws.com/nodegroup=<nodegroup-name> -o name | \
xargs kubectl cordonSafely evict all pods from the node:
kubectl drain <node-name> \
--ignore-daemonsets \
--delete-emptydir-data \
--force \
--timeout=300s💡
--ignore-daemonsetsis required since DaemonSet pods cannot be evicted.
--delete-emptydir-datais needed for pods usingemptyDirvolumes.
- Go to EC2 → Launch Templates
- Create a new version of the template with the updated EKS-optimized AMI for the target version
- Find the latest AMI:
aws ssm get-parameter \
--name /aws/service/eks/optimized-ami/<target-version>/amazon-linux-2/recommended/image_id \
--query "Parameter.Value" \
--output textAfter draining, terminate old nodes so the Auto Scaling Group launches new ones with the updated AMI:
aws ec2 terminate-instances --instance-ids <instance-id>Or update the Auto Scaling Group to perform an instance refresh:
aws autoscaling start-instance-refresh \
--auto-scaling-group-name <asg-name> \
--preferences '{"MinHealthyPercentage": 90}'kubectl get nodes
# All nodes should show the new Kubernetes versionkubectl version --short
aws eks describe-cluster --name <cluster-name> --query "cluster.version"kubectl get nodes
# All nodes should show STATUS: Readykubectl get pods -n kube-system
# All pods should be Running or Completedkubectl get pods --all-namespaces
# No pods should be in CrashLoopBackOff or Error state# Test DNS resolution
kubectl run test-dns --image=busybox --restart=Never --rm -it -- nslookup kubernetes.default
# Test pod scheduling
kubectl run test-pod --image=nginx --restart=Never
kubectl get pod test-pod
kubectl delete pod test-podaws eks list-addons --cluster-name <cluster-name>
for addon in vpc-cni coredns kube-proxy; do
aws eks describe-addon \
--cluster-name <cluster-name> \
--addon-name $addon \
--query "addon.{Name:addonName, Status:status, Version:addonVersion}"
doneCheck the following metrics in CloudWatch for anomalies:
cluster_failed_node_countnode_cpu_utilizationnode_memory_utilization- API server request error rates
EKS does not support downgrading the control plane version. Prevention is the only rollback option for the control plane.
| Component | Rollback Option |
|---|---|
| Control Plane | ❌ Not supported — must upgrade forward |
| Add-ons | ✅ Downgrade via aws eks update-addon to previous version |
| Managed Node Groups | ✅ Re-specify an older AMI release version |
| Self-Managed Nodes | ✅ Revert Launch Template to previous AMI and recycle nodes |
| Workloads | ✅ Restore from Velero/backup or revert Helm/GitOps deployment |
Before terminating drained old nodes, keep them cordoned (but running) until the new nodes pass validation. Only terminate old nodes once you are confident the upgrade is successful.
kubectl describe node <node-name>
# Check for taints, missing CNI, or kubelet errors
# Restart kubelet on the node (via SSM)
aws ssm start-session --target <instance-id>
sudo systemctl restart kubeletaws eks describe-addon --cluster-name <cluster-name> --addon-name <addon-name>
# Check "configurationSchema" and "health.issues" in output
kubectl describe pod -n kube-system -l k8s-app=<addon-label>kubectl describe pod <pod-name>
# Check Events section for scheduling failures, resource limits, or node taintsEnsure your kubectl client version is within ±1 minor version of the server:
kubectl version
# Update kubectl if needed
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"# Check for deprecated API usage
kubectl api-resources
kubectl get events --all-namespaces | grep -i deprecatedPre-Upgrade
[ ] Reviewed Kubernetes and EKS release notes
[ ] Checked deprecated API usage (pluto or kubectl-convert)
[ ] Verified add-on compatibility
[ ] Backed up cluster resources
[ ] Notified stakeholders
Control Plane
[ ] Triggered control plane upgrade
[ ] Waited for status: ACTIVE
[ ] Confirmed new server version with kubectl
Add-ons
[ ] Upgraded vpc-cni
[ ] Upgraded coredns
[ ] Upgraded kube-proxy
[ ] Upgraded other add-ons (ebs-csi, etc.)
Node Groups
[ ] Upgraded managed node groups
[ ] Cordoned, drained, and replaced self-managed nodes
[ ] Verified all nodes are Ready at new version
Post-Upgrade
[ ] Verified all system pods are healthy
[ ] Verified all application pods are healthy
[ ] Ran smoke tests
[ ] Reviewed CloudWatch metrics
Last updated: April 2026 | Compatible with EKS Kubernetes versions 1.27–1.30