Karpenter: The Better Way to Autoscale Kubernetes Nodes

Karpenter: The Better Way to Autoscale Kubernetes Nodes

If you’re running Kubernetes on a cloud provider and using the Cluster Autoscaler, Karpenter is worth your attention. Karpenter is a node provisioning project from AWS (now a CNCF project, also supported on Azure and GCP) that takes a fundamentally different approach to node scaling — and the result is faster scale-out, lower costs, and less operational overhead.

The Problem with Cluster Autoscaler

The Kubernetes Cluster Autoscaler has been around since 2016 and works like this:

  1. Pod can’t be scheduled (pending)
  2. Autoscaler looks at existing node groups
  3. Picks the cheapest node group that can fit the pod
  4. Scales that node group up by adding a node of the pre-configured type

This works, but has real limitations:

  • Node groups must be pre-configured: You define your instance types ahead of time
  • Slow scale-out: CA has to wait for the cloud provider to provision the node, register it, then reschedule the pod
  • Scale-in is conservative: CA is slow to remove nodes to avoid disruptions
  • Fixed instance families: Opportunities to use spot instances across many instance types are hard to configure

The result: over-provisioned clusters, suboptimal instance selection, and paying for capacity you’re not using.

How Karpenter Works Differently

Karpenter watches for unschedulable pods directly and provisions individual nodes optimized for those pods:

  1. Pod can’t be scheduled (pending)
  2. Karpenter analyzes the pod’s requirements (CPU, memory, GPU, labels, topology constraints)
  3. Karpenter selects the optimal instance type from a broad list
  4. Provisions that specific instance via the cloud provider API
  5. Registers it with the cluster

The key differences:

No pre-configured node groups: Karpenter considers all available instance types and picks the best fit.

Right-sized nodes: If you have 3 pending pods that together need 4 CPU and 8GB RAM, Karpenter can provision a single 4-core/8GB node rather than two smaller ones.

Spot instance optimization: Karpenter can consider dozens of instance types simultaneously, dramatically increasing spot availability and reducing interruptions.

Faster scale-out: By bypassing node groups and directly calling EC2 APIs, Karpenter provisions nodes faster.

Automatic consolidation: Karpenter actively looks for opportunities to consolidate workloads onto fewer nodes and terminate underutilized ones.

Installation on AWS EKS

# Set environment variables
export CLUSTER_NAME=my-cluster
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Create IAM role for Karpenter
cat > karpenter-node-trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role \
  --role-name KarpenterNodeRole-${CLUSTER_NAME} \
  --assume-role-policy-document file://karpenter-node-trust-policy.json

# Attach necessary policies
for policy in AmazonEKSWorkerNodePolicy AmazonEKS_CNI_Policy AmazonEC2ContainerRegistryReadOnly AmazonSSMManagedInstanceCore; do
  aws iam attach-role-policy \
    --role-name KarpenterNodeRole-${CLUSTER_NAME} \
    --policy-arn arn:aws:iam::aws:policy/$policy
done

# Install Karpenter with Helm
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version "1.1.0" \
  --namespace karpenter \
  --create-namespace \
  --set settings.clusterName=${CLUSTER_NAME} \
  --set settings.interruptionQueue=${CLUSTER_NAME} \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

Configuring NodePool and EC2NodeClass

The core configuration objects in Karpenter:

# EC2NodeClass: defines the node configuration (AMI, networking, storage)
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  role: KarpenterNodeRole-my-cluster
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  blockDeviceMappings:
  - deviceName: /dev/xvda
    ebs:
      volumeSize: 100Gi
      volumeType: gp3
      encrypted: true
      deleteOnTermination: true
---
# NodePool: defines the pool of node types and constraints
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        karpenter.sh/node-pool: default
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64", "arm64"]
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: karpenter.k8s.aws/instance-category
        operator: In
        values: ["c", "m", "r"]  # Compute, memory, memory-optimized
      - key: karpenter.k8s.aws/instance-generation
        operator: Gt
        values: ["2"]  # At least 3rd gen
      taints: []
  limits:
    cpu: 1000       # Max 1000 CPU across all nodes
    memory: 1000Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s
    budgets:
    - nodes: 10%    # Don't disrupt more than 10% of nodes at once

Spot Instances at Scale

One of Karpenter’s biggest wins is spot instance optimization:

# Separate NodePool for spot-only workloads
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: spot-workers
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot"]
      # Specify many instance types to maximize spot availability
      - key: karpenter.k8s.aws/instance-category
        operator: In
        values: ["c", "m", "r", "t"]
      - key: karpenter.k8s.aws/instance-size
        operator: In
        values: ["large", "xlarge", "2xlarge", "4xlarge"]
      taints:
      - key: spot
        value: "true"
        effect: NoSchedule
# Pod tolerating spot nodes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-processor
spec:
  replicas: 10
  template:
    spec:
      tolerations:
      - key: spot
        value: "true"
        effect: NoSchedule
      nodeSelector:
        karpenter.sh/capacity-type: spot
      containers:
      - name: processor
        image: myapp:latest
        # Make sure batch jobs checkpoint/resume on interruption

By spreading spot workloads across many instance types and sizes, Karpenter reduces interruption probability significantly compared to pinning to one instance type.

Consolidation: The Hidden Cost Savings

Karpenter’s consolidation feature actively looks for opportunities to pack workloads onto fewer nodes:

# Aggressive consolidation for development clusters
spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 60s      # Consolidate quickly
    budgets:
    - nodes: 50%               # Up to 50% churn is fine in dev
# Conservative consolidation for production
spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 5m       # Wait longer before acting
    budgets:
    - nodes: 5%                # Limit disruption
    - schedule: "0 9-17 * * MON-FRI"
      nodes: 0%                # No disruption during business hours

Consolidation works by evicting pods from underutilized nodes and rescheduling them on other nodes, then terminating the empty node. Over time, this actively reduces your node count to the minimum needed.

Observed Cost Savings

Common reported improvements after migrating from Cluster Autoscaler to Karpenter:

  • 20-40% reduction in EC2 costs from better right-sizing and spot utilization
  • 50-70% faster scale-out — Karpenter bypasses some of CA’s polling delays
  • Fewer “stuck” pending pods — CA could leave pods pending if no node group fit; Karpenter is more flexible

The exact numbers depend heavily on your workload characteristics and how well-configured your node groups were before.

When Karpenter Makes Sense

Karpenter is best for:

  • Variable workloads with unpredictable scaling needs
  • Mixed spot/on-demand requirements
  • AI/ML workloads needing specific GPU instance types
  • Development clusters you want to keep cheap overnight

Karpenter adds complexity and isn’t worth it for:

  • Small, fixed-size clusters
  • On-premises Kubernetes (though Azure and GCP support is growing)
  • Homelab clusters (no cloud provider APIs to call)

Conclusion

Karpenter represents a meaningfully better approach to node provisioning than the Cluster Autoscaler. If you’re running EKS (or AKS with the Azure provider) and have variable workloads, the combination of right-sized node provisioning, spot instance optimization, and active consolidation typically delivers 20-40% cost reduction with better scheduling performance.

The migration path from Cluster Autoscaler is well-documented and reversible. Start by running Karpenter alongside your existing node groups to get a feel for how it behaves before fully committing.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...