Amazon EKS — Cluster Autoscaler
Cluster Autoscaler adjusts the number of nodes by modifying AWS Auto Scaling Group (ASG) desired capacity. When pods can’t be scheduled due to insufficient resources, it scales up. When nodes are underutilized, it scales down.
How It Works
- Pod is created but stays Pending (no node has capacity)
- Cluster Autoscaler detects the unschedulable pod
- It evaluates which ASG can accommodate the pod
- It increases the ASG’s desired count
- ASG launches a new EC2 instance
- Node joins the cluster → pod gets scheduled
Scale-down works in reverse:
- CA checks if nodes are underutilized (< 50% utilization by default)
- It verifies pods can be rescheduled elsewhere
- It cordons and drains the node
- Decreases ASG desired count → EC2 terminates
Installation
IRSA Setup
cat > ca-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeScalingActivities",
"autoscaling:DescribeTags",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup",
"ec2:DescribeLaunchTemplateVersions",
"ec2:DescribeInstanceTypes",
"ec2:DescribeImages",
"ec2:GetInstanceTypesFromInstanceRequirements",
"eks:DescribeNodegroup"
],
"Resource": "*"
}
]
}
EOF
aws iam create-policy \
--policy-name ClusterAutoscalerPolicy \
--policy-document file://ca-policy.json
eksctl create iamserviceaccount \
--cluster my-cluster \
--namespace kube-system \
--name cluster-autoscaler \
--attach-policy-arn arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):policy/ClusterAutoscalerPolicy \
--approve
Helm Install
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
--namespace kube-system \
--set autoDiscovery.clusterName=my-cluster \
--set awsRegion=us-east-1 \
--set rbac.serviceAccount.create=false \
--set rbac.serviceAccount.name=cluster-autoscaler \
--set extraArgs.balance-similar-node-groups=true \
--set extraArgs.skip-nodes-with-system-pods=false
ASG Discovery Tags
Cluster Autoscaler discovers ASGs using tags:
k8s.io/cluster-autoscaler/enabled = true
k8s.io/cluster-autoscaler/my-cluster = owned
These are automatically added by eksctl and the EKS Terraform module for managed node groups.
Configuration Parameters
| Parameter | Default | Description |
|---|---|---|
--scale-down-utilization-threshold |
0.5 | Scale down when node < 50% utilized |
--scale-down-unneeded-time |
10m | Wait time before scale down |
--scale-down-delay-after-add |
10m | Cooldown after adding a node |
--max-graceful-termination-sec |
600 | Max time to drain a node |
--balance-similar-node-groups |
false | Balance nodes across ASGs (set to true) |
--skip-nodes-with-system-pods |
true | Don’t scale down nodes with kube-system pods |
--expander |
random | Strategy for choosing which ASG to scale: random, most-pods, least-waste, priority |
Multiple Node Groups
# Scale parameters per node group via ASG tags
# Or use priority expander
cat > priority-expander.yaml << 'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |
50:
- .*spot.*
30:
- .*on-demand.*
EOF
kubectl apply -f priority-expander.yaml
Limitations
| Limitation | Impact |
|---|---|
| Scales via ASG (indirect) | Slower — must wait for ASG, EC2, node join |
| One instance type per ASG | Less flexible — can’t mix instance types in one group |
| No bin-packing optimization | Doesn’t choose optimal instance type per workload |
| Scale-up latency | 2-5 minutes from pending pod to running |
| ASG sizing constraints | Must pre-define min/max for each ASG |
Recommendation: For new clusters, use Karpenter instead — it addresses all these limitations.
Best Practices
- Set
balance-similar-node-groups: true— distribute across AZs evenly - Use
least-wasteexpander — better bin-packing than random - Set appropriate min/max on ASGs — CA won’t exceed these limits
- Don’t set CPU limits too low — CA uses requests for scheduling, not limits
- Add PodDisruptionBudgets — CA respects PDBs during scale-down
- Consider migrating to Karpenter — faster, smarter, more cost-effective