Skip to main content

Command Palette

Search for a command to run...

Mastering Kubernetes Cluster Autoscaler on Amazon EKS: A Complete Guide

Published
โ€ข12 min read
Mastering Kubernetes Cluster Autoscaler on Amazon EKS: A Complete Guide
P
DevOps Engineer with ~3 years of hands-on experience designing, automating, and operating cloud-native infrastructure on AWS and Kubernetes (EKS). I specialize in Terraform-based IaC, CI/CD automation, GitOps with ArgoCD, observability stacks (Prometheus, Grafana, OTEL), and cloud security. I've led GCP-to-AWS migrations, built multi-environment EKS deployments for 50+ microservices, and achieved 35โ€“40% infra cost reductions using FinOps practices. Currently working at Infra360 (Gurugram) as Associate DevOps Engineer. I write about real-world DevOps challenges, cloud-native architecture, and GitOps best practices at blog.devopswithpiyush.in. ๐ŸŽ“ MCA | University Institute of Technology, Bhopal ๐Ÿ… AWS Certified Solutions Architect โ€“ Associate ๐Ÿ… Certified Kubernetes Administrator (CKA) | Linux Foundation

๐Ÿš€ TL;DR: If your pods are stuck in Pending state because there aren't enough nodes โ€” Cluster Autoscaler (CA) is your answer. This guide walks you through everything from IAM setup to a full production deployment on Amazon EKS.


๐Ÿ‘‹ Who Is This For?

Level What You'll Get
๐ŸŸข Beginner Understand what CA is and why you need it
๐ŸŸก Intermediate Full step-by-step installation on EKS
๐Ÿ”ด Advanced Multi-node group strategies, expander policies, best practices

๐Ÿค” The Problem โ€” Why Does Autoscaling Even Matter?

Imagine your application is running fine on Amazon EKS with 3 nodes. Suddenly, a traffic surge hits โ€” a flash sale, a major client onboarding, or a viral event. Your Kubernetes Deployment tries to spin up 10 more pods โ€” but there's no room on existing nodes. Those pods sit in Pending state, requests time out, and your users see errors.

You could manually add nodes โ€” but who's watching at 2 AM on a Sunday?

This is exactly where Cluster Autoscaler (CA) steps in. It watches for Pending pods and automatically scales your EC2 node count up or down via AWS Auto Scaling Groups โ€” no human intervention needed.


๐Ÿง  Section 1: What Is Cluster Autoscaler? (Beginner)

Cluster Autoscaler is an open-source Kubernetes component that runs as a Deployment inside your cluster (in the kube-system namespace). It does two things:

  • Scale Up ๐Ÿ“ˆ โ€” When pods are unschedulable (Pending), CA adds new EC2 nodes

  • Scale Down ๐Ÿ“‰ โ€” When nodes are underutilized, CA safely drains and removes them

How It Works (Every 10 Seconds)

  1. Are there any Pending pods?
    YES โ†’ Find a Node Group that can fit them โ†’ Tell ASG to increase capacity

  2. Are any nodes underutilized (< 50% by default)?
    YES โ†’ Can all pods fit elsewhere? โ†’ Drain node โ†’ Terminate EC2 instance

๐Ÿ’ก Key Insight: CA doesn't look at CPU/Memory usage. It looks at resource REQUESTS defined in your pod spec. Always set resources.requests or CA won't scale!

CA vs HPA vs Karpenter

Tool What it Scales How
HPA Pod replicas Based on CPU/memory metrics
CA EC2 Nodes Based on pending pods + AWS ASG
Karpenter EC2 Nodes Dynamic, just-in-time, more flexible

Think of it this way: HPA scales your app. CA scales your infrastructure.


๐Ÿ—๏ธ Section 2: Architecture Overview

The OIDC + IRSA bridge is the key โ€” it lets the CA pod (inside Kubernetes) make authenticated AWS API calls without storing any long-lived credentials.


๐Ÿ› ๏ธ Section 3: Full Installation Guide (Intermediate)

Prerequisites Checklist

Before you begin, make sure you have:

  • โœ… An active Amazon EKS Cluster (v1.24+)

  • โœ… kubectl configured and pointing to your cluster

  • โœ… eksctl installed (v0.160+)

  • โœ… aws cli v2 configured with admin permissions

  • โœ… Node Groups created with ASG enabled (--asg-access flag)


Step 1: Enable IAM OIDC Provider

OIDC is an identity bridge โ€” it lets Kubernetes ServiceAccounts assume AWS IAM Roles, so your CA pod can call AWS APIs securely without hardcoding credentials.

export CLUSTER_NAME=<your-cluster-name>
export AWS_REGION=ap-south-1   # Change to your region

# Enable OIDC for your cluster
eksctl utils associate-iam-oidc-provider \
  --region $AWS_REGION \
  --cluster $CLUSTER_NAME \
  --approve

# Verify
aws eks describe-cluster --name $CLUSTER_NAME \
  --query "cluster.identity.oidc.issuer" --output text

Step 2: Create IAM Policy

Save the following as iam-policy.json. This policy defines exactly what CA is allowed to do in AWS โ€” describe ASGs, set desired capacity, and terminate instances.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "autoscaling:DescribeAutoScalingGroups",
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeLaunchConfigurations",
                "autoscaling:DescribeTags",
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup",
                "ec2:DescribeLaunchTemplateVersions"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

aws iam create-policy
--policy-name AmazonEKSClusterAutoscalerPolicy
--policy-document file://iam-policy.json

Note down the Policy ARN from the output โ€” you'll need it in the next step.

Step 3: Create IAM Role + Kubernetes ServiceAccount (IRSA)

IRSA (IAM Roles for Service Accounts) annotates a Kubernetes ServiceAccount with an IAM Role ARN, so only the CA pod gets AWS permissions โ€” nothing else.

export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

eksctl create iamserviceaccount \
  --cluster=$CLUSTER_NAME \
  --namespace=kube-system \
  --name=cluster-autoscaler \
  --attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AmazonEKSClusterAutoscalerPolicy \
  --override-existing-serviceaccounts \
  --approve

If you prefer to apply the ServiceAccount manually, save this as cluster-autoscaler-sa.yaml and replace the role ARN:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<YOUR-ACCOUNT-ID>:role/<YOUR-IAM-ROLE-NAME>
kubectl apply -f cluster-autoscaler-sa.yaml

Step 4: Apply RBAC โ€” ClusterRole, Role, and Bindings

Save the following as cluster-autoscaler-rbac.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["events", "endpoints"]
  verbs: ["create", "patch"]
- apiGroups: [""]
  resources: ["pods/eviction"]
  verbs: ["create"]
- apiGroups: [""]
  resources: ["pods/status"]
  verbs: ["update"]
- apiGroups: [""]
  resources: ["endpoints"]
  resourceNames: ["cluster-autoscaler"]
  verbs: ["get", "update"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["watch", "list", "get", "update"]
- apiGroups: [""]
  resources: ["namespaces", "pods", "services", "replicationcontrollers", "persistentvolumeclaims", "persistentvolumes"]
  verbs: ["watch", "list", "get"]
- apiGroups: ["extensions"]
  resources: ["replicasets", "daemonsets"]
  verbs: ["watch", "list", "get"]
- apiGroups: ["policy"]
  resources: ["poddisruptionbudgets"]
  verbs: ["watch", "list"]
- apiGroups: ["apps"]
  resources: ["statefulsets", "replicasets", "daemonsets"]
  verbs: ["watch", "list", "get"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"]
  verbs: ["watch", "list", "get"]
- apiGroups: ["batch", "extensions"]
  resources: ["jobs"]
  verbs: ["get", "list", "watch", "patch"]
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["create"]
- apiGroups: ["coordination.k8s.io"]
  resourceNames: ["cluster-autoscaler"]
  resources: ["leases"]
  verbs: ["get", "update"]
***
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["create", "list", "watch"]
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
  verbs: ["delete", "get", "update", "watch"]
***
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-autoscaler
subjects:
- kind: ServiceAccount
  name: cluster-autoscaler
  namespace: kube-system
***
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cluster-autoscaler
subjects:
- kind: ServiceAccount
  name: cluster-autoscaler
  namespace: kube-system
kubectl apply -f cluster-autoscaler-rbac.yaml

Step 5: Deploy Cluster Autoscaler

Save the following as cluster-autoscaler-deployment.yaml.

โš ๏ธ Replace <YOUR-CLUSTER-NAME> on the --node-group-auto-discovery line.
โš ๏ธ Match the image version (
v1.27.3 in example) to your EKS cluster version. E.g., EKS 1.30 โ†’ use v1.30.x

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    app: cluster-autoscaler
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cluster-autoscaler
  template:
    metadata:
      labels:
        app: cluster-autoscaler
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '8085'
        cluster-autoscaler.kubernetes.io/safe-to-evict: 'false'
    spec:
      priorityClassName: system-cluster-critical
      serviceAccountName: cluster-autoscaler
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: cluster-autoscaler
          image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.27.3
          resources:
            limits:
              cpu: 100m
              memory: 600Mi
            requests:
              cpu: 100m
              memory: 600Mi
          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=aws
            - --skip-nodes-with-local-storage=false
            - --expander=least-waste
            - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME>
            - --balance-similar-node-groups
            - --skip-nodes-with-system-pods=false
            - --scale-down-delay-after-add=10m
            - --scale-down-unneeded-time=10m
          volumeMounts:
            - name: ssl-certs
              mountPath: /etc/ssl/certs/ca-certificates.crt
              readOnly: true
          imagePullPolicy: "Always"
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: [ALL]
            readOnlyRootFilesystem: true
      volumes:
        - name: ssl-certs
          hostPath:
            path: "/etc/ssl/certs/ca-bundle.crt"
kubectl apply -f cluster-autoscaler-deployment.yaml

Step 6: Tag Your ASG Node Groups

CA uses tags to discover which Auto Scaling Groups it should manage. Add these two tags to your Node Group's ASG in AWS Console or CLI:

Tag Key Tag Value
k8s.io/cluster-autoscaler/enabled true
k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME> owned
aws autoscaling create-or-update-tags \
  --tags \
  "ResourceId=<YOUR-ASG-NAME>,ResourceType=auto-scaling-group,Key=k8s.io/cluster-autoscaler/enabled,Value=true,PropagateAtLaunch=true" \
  "ResourceId=<YOUR-ASG-NAME>,ResourceType=auto-scaling-group,Key=k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME>,Value=owned,PropagateAtLaunch=true"

Step 7: Verify Everything Is Working

# Check the pod is Running
kubectl get pods -n kube-system | grep cluster-autoscaler

# Watch live logs
kubectl logs -f deployment/cluster-autoscaler -n kube-system

๐Ÿ—‚๏ธ Section 4: Node Group Strategies โ€” The "NodePool" Equivalent (Advanced)

Unlike Karpenter (which has NodePool and EC2NodeClass CRDs), CA works with pre-defined EKS Node Groups (ASGs).

Karpenter Concept CA Equivalent
EC2NodeClass Launch Template
NodePool EKS Managed Node Group (ASG)
NodePool limits ASG Min/Max size
NodePool labels/taints Node Group labels & taints

Here's a production-ready multi-node-group config using eksctl. Save as production-nodegroups.yaml:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: <YOUR-CLUSTER-NAME>
  region: ap-south-1

managedNodeGroups:

  # Pool 1: General Purpose (always-on baseline)
  - name: general-ng
    instanceType: m5.xlarge
    minSize: 2
    maxSize: 10
    desiredCapacity: 2
    labels:
      workload: general
      lifecycle: on-demand
    tags:
      k8s.io/cluster-autoscaler/enabled: "true"
      k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME>: "owned"
    iam:
      withAddonPolicies:
        autoScaler: true

  # Pool 2: High Memory (scale from zero for data workloads)
  - name: highmem-ng
    instanceType: r5.2xlarge
    minSize: 0
    maxSize: 5
    desiredCapacity: 0
    labels:
      workload: high-memory
    taints:
      - key: dedicated
        value: high-memory
        effect: NoSchedule
    tags:
      k8s.io/cluster-autoscaler/enabled: "true"
      k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME>: "owned"
      k8s.io/cluster-autoscaler/node-template/label/workload: "high-memory"
      k8s.io/cluster-autoscaler/node-template/taint/dedicated: "high-memory:NoSchedule"

  # Pool 3: Spot Instances (cost savings for batch/non-critical)
  - name: spot-ng
    instanceTypes: ["m5.xlarge", "m5a.xlarge", "m4.xlarge"]
    spot: true
    minSize: 0
    maxSize: 20
    desiredCapacity: 0
    labels:
      lifecycle: spot
      workload: batch
    tags:
      k8s.io/cluster-autoscaler/enabled: "true"
      k8s.io/cluster-autoscaler/<YOUR-CLUSTER-NAME>: "owned"
eksctl create nodegroup -f production-nodegroups.yaml

Scheduling Pods to Specific Node Groups

# Example: Schedule a high-memory pod to the highmem-ng pool
spec:
  nodeSelector:
    workload: high-memory
  tolerations:
    - key: dedicated
      value: high-memory
      effect: NoSchedule
  containers:
    - name: app
      image: your-image:latest
      resources:
        requests:           # REQUIRED for CA to work!
          cpu: "2"
          memory: "8Gi"

โš™๏ธ Section 5: Expander Strategies

When multiple node groups can accommodate a pending pod, CA uses an Expander to decide which one to pick:

Expander Behavior Best For
least-waste Picks group with least wasted resources after scaling Recommended
random Picks randomly Testing only
most-pods Picks group that schedules the most pods High-density
priority You assign priority order to node groups Fine-grained control
price Prefers cheapest node type Cost-sensitive

Set it in your deployment:

- --expander=least-waste

Section 6: Test Your Setup

# Create a deployment that will trigger scale-up
kubectl create deployment inflate \
  --image=public.ecr.aws/eks-distro/kubernetes/pause:3.7 \
  --replicas=10

kubectl set resources deployment inflate \
  --requests=cpu=1,memory=1Gi

# Watch pods โ€” some will go Pending, then get scheduled on new nodes
kubectl get pods -w

# Watch CA logs in real-time
kubectl logs -f deployment/cluster-autoscaler -n kube-system | grep -E "scale_up|ScaleUp"

# Watch new nodes join
kubectl get nodes -w

# Cleanup โ€” triggers scale-down after ~10 minutes
kubectl delete deployment inflate

Section 7: Production Best Practices

  1. Always set resources.requests โ€” CA is blind without them; it won't scale if requests aren't defined

  2. Use PodDisruptionBudgets (PDB) โ€” Protects critical pods during scale-down draining

  3. Pin CA version to EKS version โ€” Use v1.30.x for EKS 1.30; version mismatch breaks scaling

  4. Use --balance-similar-node-groups โ€” Spreads nodes evenly across AZs for high availability

  5. Add safe-to-evict: "false" on CA pod itself โ€” Prevents it from being evicted during scale-down

  6. Don't mix instance families in one ASG โ€” Keep node groups homogeneous for predictable scaling

  7. Monitor with Prometheus โ€” CA exposes metrics on port 8085; scrape and alert on scaling events

Section 8: Troubleshooting

Issue Likely Cause Fix
Pods stuck in Pending, no new nodes ASG tags missing or wrong Verify tags on your ASG match --node-group-auto-discovery
Permission denied errors in logs IAM Role misconfigured Check role trust relationship + OIDC annotation on ServiceAccount
CrashLoopBackOff on CA pod Wrong image version or bad command flags Match image to EKS version; check --node-group-auto-discovery flag
Scale-down not happening scale-down-unneeded-time not elapsed or PDB blocking Wait 10 min; check PodDisruptionBudgets
Scale-from-zero not working Node group labels missing as ASG tags Add node-template/label/ and node-template/taint/ tags to ASG
# Always start debugging here
kubectl logs -n kube-system deployment/cluster-autoscaler

CA vs Karpenter โ€” Which One Should You Use in 2026?

Factor Cluster Autoscaler Karpenter
Setup Complexity Moderate Higher
Scaling Speed 2โ€“5 min 30โ€“60 sec
Instance Flexibility Fixed per ASG Dynamic, any type
Cost Optimization Good with Spot Excellent (node consolidation)
EKS Auto Mode support No Yes (native)
Maturity & Stability โญโญโญโญโญ Battle-tested โญโญโญโญ Growing fast

Wrapping Up

Cluster Autoscaler is the backbone of production Kubernetes infrastructure on AWS. Set it up correctly with proper Node Groups, IRSA, and resource requests โ€” and it will silently keep your cluster right-sized, saving both cost and on-call headaches.

Key Takeaways:

  • ๐Ÿ” OIDC + IRSA = Secure, credential-free AWS authentication from Kubernetes

  • ๐Ÿ—‚๏ธ Node Groups = Your pre-defined capacity pools (CA's version of Karpenter's NodePools)

  • ๐Ÿ“ฆ Always set resources.requests โ€” CA depends on it entirely

  • โš–๏ธ Use least-waste expander for cost efficiency

  • ๐Ÿ“Š Watch CA logs โ€” they're incredibly detailed and tell you exactly what's happening

More from this blog

DevOps with Piyush

12 posts