Amazon EKS Cluster Autoscaler - The Complete Step-by-Step Guide
Running Kubernetes workloads is simple while your traffic is predictable. The real challenge begins when demand changes throughout the day. If the cluster has too few nodes, new Pods remain in the Pending state. If it has too many nodes, you continue paying for EC2 capacity that your applications do not need.
Kubernetes Cluster Autoscaler solves this problem by adjusting the desired capacity of the EC2 Auto Scaling Groups behind your EKS node groups. When Pods cannot be scheduled because the cluster has insufficient resources, it requests additional nodes. When nodes remain underutilized and their workloads can safely run elsewhere, it allows those nodes to be removed.
In this guide, we are going to build a secure and production-aware Cluster Autoscaler setup for Amazon EKS. We will use EKS Pod Identity instead of storing AWS credentials inside Kubernetes, install the upstream Helm chart, trigger a real scale-up event, verify scale-down, and troubleshoot the problems you are most likely to encounter.
Cluster Autoscaler, HPA, and Karpenter
These tools solve different scaling problems, so it is important not to confuse them.
| Component | What it scales | Typical trigger |
|---|---|---|
| Horizontal Pod Autoscaler | Number of application Pods | CPU, memory, or custom metrics |
| Vertical Pod Autoscaler | CPU and memory assigned to Pods | Historical and current resource usage |
| Cluster Autoscaler | Nodes inside existing node groups | Unschedulable Pods or underutilized nodes |
| Karpenter | EC2 capacity selected directly for workload requirements | Unschedulable Pods and consolidation opportunities |
| EKS Auto Mode | AWS-managed cluster compute and other EKS capabilities | Workload and infrastructure demand |
The Horizontal Pod Autoscaler and Cluster Autoscaler are commonly used together. HPA creates additional Pods when application demand increases. If those Pods no longer fit on the existing nodes, Cluster Autoscaler adds node capacity.
Cluster Autoscaler works with node groups and their EC2 Auto Scaling Groups. It changes the desired capacity but respects the minimum and maximum values already configured on each group. Karpenter and EKS Auto Mode use a different provisioning model and are worth considering when you need more flexible, workload-aware instance selection.
This tutorial is for an existing EKS cluster that uses EC2 managed or self-managed node groups. Do not install a second node autoscaler to control the same capacity.
Prerequisites
Before starting, make sure you have:
- An existing Amazon EKS cluster with at least one EC2 node group
- Permission to manage IAM roles, IAM policies, EKS add-ons, and Pod Identity associations
- AWS CLI v2 installed and authenticated
kubectlconfigured for the target EKS cluster- Helm 3 installed
- Permission to inspect and tag the node group’s EC2 Auto Scaling Group
Confirm the tools and current AWS identity:
1 | aws --version |
Configure the Working Environment
Set reusable variables for your environment. Replace the example cluster, Region, and node group names before continuing.
1 | export AWS_REGION="eu-west-2" |
Verify that you are connected to the intended cluster:
1 | kubectl config current-context |
Never rely only on a shell variable before changing production infrastructure. Confirm the account, Region, cluster name, and current Kubernetes context.
Check the Node Group Scaling Boundaries
Cluster Autoscaler cannot grow a node group beyond its configured maximum size or reduce it below its minimum size.
1 | aws eks describe-nodegroup \ |
For a small lab, you might use a minimum of 1, desired capacity of 2, and maximum of 4:
1 | aws eks update-nodegroup-config \ |
Do not copy these limits directly into production. Choose values from workload demand, Availability Zone requirements, IP address capacity, service quotas, disruption tolerance, and budget.
Wait until the update is complete:
1 | aws eks wait nodegroup-active \ |
Enable Auto-Discovery Tags
Cluster Autoscaler uses tags to discover the Auto Scaling Groups it is allowed to manage. First, retrieve the ASG name behind the managed node group:
1 | export ASG_NAME="$(aws eks describe-nodegroup \ |
Apply the two discovery tags:
1 | aws autoscaling create-or-update-tags \ |
Confirm them:
1 | aws autoscaling describe-tags \ |
Repeat this step for every node group that Cluster Autoscaler should control. The cluster-specific tag prevents an autoscaler in one EKS cluster from modifying an Auto Scaling Group that belongs to another cluster.
Install the EKS Pod Identity Agent
EKS Pod Identity lets the Cluster Autoscaler Pod receive temporary AWS credentials through its Kubernetes ServiceAccount. No static AWS access key is stored in a Secret, values file, or container environment variable.
Check whether the agent is already installed:
1 | aws eks describe-addon \ |
If AWS returns ResourceNotFoundException, create the add-on:
1 | aws eks create-addon \ |
Wait for it to become active and verify the DaemonSet Pods:
1 | aws eks wait addon-active \ |
EKS Auto Mode already includes Pod Identity functionality, so its agent does not need to be installed separately. For clusters with private nodes, ensure the nodes can reach the EKS Auth API, using the required interface endpoint when there is no suitable outbound route.
Create the Cluster Autoscaler IAM Policy
Cluster Autoscaler needs read access to inspect EC2 and Auto Scaling configuration. It also needs permission to change desired capacity and terminate an instance during scale-down.
Create cluster-autoscaler-policy.json:
1 | { |
Replace codingtricks-eks in the IAM condition key with your actual cluster name. IAM policy JSON does not expand shell variables.
Create the policy:
1 | aws iam create-policy \ |
The modifying actions use tag-based conditions, while read-only discovery calls use Resource: "*" because those APIs do not support the same resource-level restrictions. This prevents the role from scaling an unrelated ASG that does not carry both expected tags.
Create the Pod Identity IAM Role
Create cluster-autoscaler-trust-policy.json:
1 | { |
Create the role and attach the policy:
1 | aws iam create-role \ |
The trust policy is intentionally different from an IRSA trust policy. EKS Pod Identity uses the pods.eks.amazonaws.com service principal and does not require an OIDC provider or a role annotation on the ServiceAccount.
Create the ServiceAccount and Pod Identity Association
Create the ServiceAccount that the Helm release will reuse:
1 | kubectl create serviceaccount "$CA_SERVICE_ACCOUNT" \ |
Create the Pod Identity association:
1 | aws eks create-pod-identity-association \ |
Confirm the association:
1 | aws eks list-pod-identity-associations \ |
There is no IAM role annotation to check with Pod Identity. The association is stored in EKS rather than inside the Kubernetes ServiceAccount object.
Select a Compatible Cluster Autoscaler Version
Cluster Autoscaler contains scheduler simulation logic, so its minor version must match the Kubernetes control-plane minor version. Cross-version compatibility is not tested or supported by the project.
Retrieve the EKS version:
1 | export K8S_VERSION="$(aws eks describe-cluster \ |
Choose the latest supported Cluster Autoscaler patch release with the same minor version. For example, Kubernetes 1.34.x requires Cluster Autoscaler 1.34.x.
1 | # Example for an EKS 1.34 cluster. At the time of writing, 1.34.5 is the |
Do not blindly use latest. Review the upstream compatibility table and release notes during every EKS upgrade.
Install Cluster Autoscaler with Helm
Add the official chart repository:
1 | helm repo add autoscaler https://kubernetes.github.io/autoscaler |
Create cluster-autoscaler-values.yaml:
1 | cloudProvider: aws |
Replace the Region, cluster name, and image tag with the values you verified earlier. Then install the chart:
1 | helm upgrade --install cluster-autoscaler \ |
The main values do the following:
autoDiscovery.clusterNamediscovers only ASGs carrying the matching tags.fullnameOverridegives the Deployment a predictable name for the verification commands used below.rbac.serviceAccount.create=falsereuses the ServiceAccount associated with the IAM role.image.tagpins Cluster Autoscaler to the Kubernetes-compatible minor version.least-wastechooses the node group that leaves the least unused capacity after scale-up.balance-similar-node-groupshelps distribute capacity across equivalent node groups.safe-to-evict=falseprevents Cluster Autoscaler from evicting its own Pod during node consolidation.system-cluster-criticalgives the controller a high scheduling priority.
Setting skip-nodes-with-system-pods=false allows nodes containing movable kube-system Pods to be considered for scale-down. Test this carefully with your add-ons and PodDisruptionBudgets. We have intentionally not disabled the local-storage safety check; workloads that keep irreplaceable data on node-local storage need special attention.
Verify the Installation
Check the Helm release, Deployment, ServiceAccount, and Pod:
1 | helm list --namespace "$CA_NAMESPACE" |
Inspect the logs:
1 | kubectl logs \ |
Healthy logs should show that the AWS cloud provider initialized and one or more Auto Scaling Groups were discovered. You should not see AccessDenied, credential-provider, or repeated ASG discovery errors.
You can also inspect the status ConfigMap:
1 | kubectl get configmap cluster-autoscaler-status \ |
Test a Real Scale-Up Event
We need Pods with resource requests large enough to exceed current node capacity. Create inflate.yaml:
1 | apiVersion: apps/v1 |
Apply the Deployment and increase the replica count:
1 | kubectl apply -f inflate.yaml |
Adjust the replica count for your instance type and lab limits. Do not trigger a large test in a production cluster without a controlled change window and cost approval.
Watch the Pods:
1 | kubectl get pods \ |
In a second terminal, watch the nodes:
1 | kubectl get nodes --watch |
Some Pods should initially remain Pending with an insufficient resource message:
1 | kubectl get pods --selector app=inflate |
Follow the autoscaler’s decisions:
1 | kubectl logs \ |
Finally, confirm that the ASG desired capacity increased:
1 | aws autoscaling describe-auto-scaling-groups \ |
The EC2 instance must launch, bootstrap, and join the cluster before the waiting Pods can run. The complete process normally takes longer than the autoscaler’s initial decision.
Test Scale-Down
Remove the test workload demand:
1 | kubectl scale deployment inflate --replicas=0 |
Watch the autoscaler logs and node list:
1 | kubectl logs \ |
1 | kubectl get nodes --watch |
By default, a node normally needs to remain unneeded for a period before it becomes eligible for scale-down. Cluster Autoscaler will not reduce the group below its minimum size, and it will not remove a node when its remaining Pods cannot be safely moved.
After the test, delete the Deployment:
1 | kubectl delete -f inflate.yaml |
Common Problems and Troubleshooting
Cluster Autoscaler Shows AccessDenied
Confirm the Pod is using the expected ServiceAccount:
1 | kubectl get deployment cluster-autoscaler \ |
Then verify:
- The EKS Pod Identity Agent is running on the node.
- The Pod Identity association uses the same cluster, namespace, and ServiceAccount.
- The IAM role has the autoscaler policy attached.
- The role trust policy allows
pods.eks.amazonaws.com. - The node role permits
eks-auth:AssumeRoleForPodIdentity. - Private nodes can reach the EKS Auth API.
Restart the Deployment after correcting the association:
1 | kubectl rollout restart deployment cluster-autoscaler \ |
The Node Group Is Not Discovered
Inspect the rendered Deployment arguments:
1 | kubectl get deployment cluster-autoscaler \ |
Confirm that:
--cloud-provider=awsis present.- The auto-discovery argument includes both expected tag keys.
- The ASG carries
k8s.io/cluster-autoscaler/enabled=true. - The ASG carries
k8s.io/cluster-autoscaler/<cluster-name>=owned. - The cluster name in Helm values matches the real EKS cluster name exactly.
A Pending Pod Does Not Trigger Scale-Up
First, read its scheduler events:
1 | kubectl describe pod <pending-pod-name> |
A new node helps only when a discovered node group can satisfy the Pod. Check for:
- A resource request larger than any available instance type
- Node selectors or required node affinity that no node group provides
- Taints without matching tolerations
- Topology or Availability Zone constraints
- An unbound PersistentVolumeClaim
- Host-port conflicts
- A node group already at maximum size
- EC2 capacity shortages, service quotas, or exhausted subnet IP addresses
For a node group that can scale from zero, publish its expected labels, taints, and extended resources as ASG node-template tags when the autoscaler cannot infer them from the launch template.
Nodes Scale Up but Pods Stay Pending
The scaling API call succeeded, but the scheduling requirement is still not satisfied. Compare the Pod requests and constraints with the new node:
1 | kubectl describe pod <pending-pod-name> |
Check the node’s allocatable CPU and memory after DaemonSets, labels, taints, EBS volume zone, ENI/IP capacity, and the EC2 instance type actually launched by a mixed instances policy.
Nodes Do Not Scale Down
Use the logs to find the exact blocker:
1 | kubectl logs \ |
Common causes include:
- The node group is already at its minimum size.
- A PodDisruptionBudget does not allow another disruption.
- A Pod has
cluster-autoscaler.kubernetes.io/safe-to-evict: "false". - A Pod uses local storage and the relevant safety setting blocks eviction.
- A system Pod cannot run elsewhere.
- Affinity, anti-affinity, topology, or resource requests prevent rescheduling.
- A bare Pod is not managed by a controller.
Do not weaken eviction protections only to make a node disappear. Fix the workload’s availability and scheduling design first.
Scale-Up Stops at a Specific Number of Nodes
Inspect the node group and ASG limits:
1 | aws eks describe-nodegroup \ |
If the group is at maxSize, Cluster Autoscaler is behaving correctly. Increasing the limit changes your cost and capacity risk, so treat it as an infrastructure change rather than a troubleshooting shortcut.
Production Best Practices
Match Versions During Every Upgrade
Upgrade Cluster Autoscaler as part of the EKS control-plane upgrade plan. Keep the Kubernetes and Cluster Autoscaler minor versions aligned, then test scale-up and scale-down before considering the upgrade complete.
Set Realistic Resource Requests
Cluster Autoscaler makes scheduling decisions from requests. Missing or severely underestimated requests create poor bin-packing decisions, while exaggerated requests can cause unnecessary nodes to launch. Use metrics and VPA recommendations to refine them.
Keep Node Groups Consistent
Nodes in one group should have equivalent scheduling properties. When using a mixed instances policy, select instance types with a similar CPU, memory, and GPU shape. Cluster Autoscaler simulates scheduling using the first instance type in the policy; smaller alternatives may still leave Pods pending, while larger alternatives can waste capacity.
Separate Spot and On-Demand Capacity
Spot and On-Demand nodes have different availability and scheduling characteristics. Separate them into different node groups, apply appropriate labels or taints, and use Pod tolerations and disruption controls. least-waste is a practical general-purpose expander, while the priority expander is useful when one capacity type should be attempted before another.
Protect Critical Workloads
Use multiple replicas, topology spread constraints, Pod anti-affinity where appropriate, and realistic PodDisruptionBudgets. Apply the safe-to-evict annotation only when eviction genuinely creates unacceptable risk because excessive protection can prevent cluster scale-down indefinitely.
Monitor the Autoscaler
At minimum, alert on:
- Cluster Autoscaler Pod unavailable or repeatedly restarting
- Unschedulable Pods persisting beyond the expected node launch time
- Node groups remaining at maximum capacity
- Repeated AWS API errors or throttling
- Failed node launches or nodes that do not join the cluster
- Nodes marked unneeded but never removed
- Unexpected changes in desired capacity and EC2 cost
Collect Cluster Autoscaler logs centrally and retain Kubernetes events long enough to investigate incidents after they occur.
Plan Capacity Outside Kubernetes
Autoscaling cannot fix every infrastructure limit. Monitor subnet IP availability, EC2 quotas, Spot capacity, IAM failures, launch-template errors, bootstrap failures, and dependencies required by private nodes. Keep enough minimum capacity for critical system workloads and failure scenarios.
Tune Conservatively
Flags such as scan interval, scale-down delay, utilization threshold, and node provisioning timeout affect cost, availability, and AWS API usage. Start with upstream defaults, measure actual behaviour, and change one setting at a time. Aggressive scale-down can save money but also increase churn and application disruption.
Cluster Autoscaler or Karpenter?
Cluster Autoscaler remains a good choice when:
- You already manage stable EKS node groups.
- Your organization understands ASGs and managed node groups.
- Capacity types and instance shapes are deliberately predefined.
- You want a mature, portable Kubernetes autoscaling model.
Consider Karpenter or EKS Auto Mode when:
- You need more flexible instance selection for individual workloads.
- Faster, just-in-time provisioning is important.
- You want automated consolidation and right-sizing across a broad EC2 catalog.
- Maintaining many narrowly defined node groups has become operationally expensive.
Do not let two controllers independently manage the same nodes. Plan and test any migration so ownership of compute capacity is always clear.
Environment Cleanup
The following commands remove the tutorial workload and Cluster Autoscaler configuration. Confirm the account, Region, cluster, role, and policy names before running them.
Remove the test Deployment and Helm release:
1 | kubectl delete deployment inflate --ignore-not-found |
Find the Pod Identity association ID:
1 | export ASSOCIATION_ID="$(aws eks list-pod-identity-associations \ |
After confirming the value, delete the association and ServiceAccount:
1 | aws eks delete-pod-identity-association \ |
Detach and delete the IAM resources only if no other workload uses them:
1 | aws iam detach-role-policy \ |
Optionally remove the ASG discovery tags if the group will no longer be managed by Cluster Autoscaler:
1 | aws autoscaling delete-tags \ |
Do not remove the EKS Pod Identity Agent when other workloads use Pod Identity. You may also want to restore the node group’s original minimum, desired, and maximum capacity after a lab test.
Conclusion
In this article, we learned how to configure Kubernetes Cluster Autoscaler on Amazon EKS using ASG auto-discovery, least-privilege IAM permissions, EKS Pod Identity, and the official Helm chart. We also created a controlled workload to verify real scale-up and scale-down behaviour, examined the most common failure modes, and reviewed the production decisions that affect security, availability, and cost.
Cluster Autoscaler works best when the surrounding platform is designed correctly: workloads have realistic resource requests, node groups expose consistent scheduling properties, disruption policies allow safe movement, and AWS capacity limits are monitored. With those foundations in place, it can keep your EKS cluster responsive without leaving unnecessary EC2 instances running.
Are you currently using Cluster Autoscaler, Karpenter, or EKS Auto Mode? Share your experience or any issue you faced in the comments.





