How to Set Up Karpenter for EKS Autoscaling: 12 Steps [2026]

Kubernetes clusters waste money in a very specific way: they hold onto nodes that pending pods no longer need, and they lean on Cluster Autoscaler’s coarse-grained node group logic instead of picking the right instance for the job. Karpenter, the open source node provisioning project originally built by AWS, fixes both problems by launching exactly the EC2 instance type a workload needs and tearing it down the moment it’s idle. As of August 2026, the project sits at version 1.13.0 on the aws/karpenter-provider-aws repository, and it has become the default recommendation in AWS’s own EKS best-practices guidance for teams running mixed or bursty workloads.

This tutorial walks through a full Karpenter install on Amazon EKS, from a bare cluster to a working NodePool that provisions Spot and On-Demand instances automatically, handles Spot interruptions gracefully, and consolidates underutilized nodes without you touching an Auto Scaling Group. It assumes you already have an EKS cluster or are comfortable creating one, and it includes the IAM wiring, the CRDs, the YAML, and the troubleshooting steps that trip up most first-time installs.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Karpenter Actually Does Differently From Cluster Autoscaler

Cluster Autoscaler watches for unschedulable pods, then scales an existing Auto Scaling Group up or down. It’s tied to whatever instance types you predefined in that node group, so if your workload needs more memory than the group’s instance type offers, the pod stays pending no matter how many nodes get added. Karpenter skips the node group layer entirely. It watches the same unschedulable pods, but it calculates the actual CPU, memory, and GPU requirements of the pending workload and then calls the EC2 Fleet API directly to launch the cheapest instance type that fits, drawing from dozens of possible instance families rather than one preset list.

That difference shows up directly in cloud bills. Salesforce reported roughly 5% in FY2026 infrastructure savings after migrating from Cluster Autoscaler to Karpenter across 1,000 EKS clusters, with another 5% to 10% projected for FY2027 as the rollout continues. Prodigy Education reported 40% to 60% compute cost reductions on EKS after adopting Karpenter, and DevOps consultancy Armakuni documented monthly EC2 costs falling from the $4,200 to $4,400 range down to $2,400 to $2,600, a 42% cut, largely from tighter bin-packing and more aggressive Spot usage. None of these numbers are guaranteed for every workload, but they explain why Karpenter has become the standard answer to “why is my EKS bill so high.”

The mechanism behind those savings is consolidation. Karpenter continuously checks whether a cheaper instance type could run the same pods, or whether multiple partially-empty nodes could be replaced by fewer, denser ones. When it finds an opportunity, it cordons the old node, reschedules the pods, and terminates the excess capacity, all without you writing a scaling policy. Cluster Autoscaler can scale down empty nodes, but it doesn’t repack live nodes to free up more space, which is the gap Karpenter closes.

There’s also a latency argument that rarely gets enough attention. Cluster Autoscaler’s decisions run through an Auto Scaling Group launch cycle, which typically takes three to five minutes from “pod is pending” to “node is Ready,” because the ASG has to launch an instance from a template, wait for it to pass health checks, and then let the Kubernetes node bootstrap and register. Karpenter calls the EC2 Fleet API directly and skips the ASG layer entirely, which is why most teams see new capacity land in 30 to 90 seconds. For workloads with spiky, unpredictable traffic, such as a checkout service during a flash sale or a batch job that fans out to hundreds of pods at 2 a.m., that latency gap is often more valuable than the raw dollar savings, since it’s the difference between autoscaling that keeps up with demand and one that’s always a few minutes behind it.

Prerequisites and Versions for This Karpenter Setup

Confirm each of these before starting. Version mismatches are the single most common cause of a broken Karpenter install, because the Helm chart version and the CRD version must match exactly.

  • An existing Amazon EKS cluster running Kubernetes 1.30 or later (Karpenter 1.13.0’s stable v1 NodePool and EC2NodeClass APIs assume a 1.30+ control plane in current AWS guidance)
  • AWS CLI v2, configured with an IAM identity that has administrator or near-administrator access to the account for the initial setup
  • kubectl matched to your cluster’s Kubernetes minor version
  • Helm 3.14 or later
  • eksctl 0.190 or later (optional but strongly recommended, since it automates most of the IAM and CloudFormation wiring this tutorial covers manually)
  • An EKS cluster with at least one existing node group or Fargate profile to run the Karpenter controller pods themselves (Karpenter cannot bootstrap the very first node it needs to run on)
  • Permissions to create IAM roles, an SQS queue, and EventBridge rules in the target AWS account

Karpenter 1.13.0 was tagged as the latest stable release on the project’s GitHub releases page on June 10, 2026. Always check the current releases page before you install, since AWS ships patch releases regularly and version pins to specific EKS releases can shift.

Step 1: Set Environment Variables for the Install

Every command in this tutorial references the same handful of values, so export them once at the top of your shell session to avoid typos later.

export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.13.0"
export K8S_VERSION="1.30"
export AWS_PARTITION="aws"
export CLUSTER_NAME="my-eks-cluster"
export AWS_DEFAULT_REGION="us-east-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export TEMPOUT="$(mktemp)"
export ALIAS_VERSION="$(aws ssm get-parameter --name "/aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/x86_64/standard/recommended/image_id" --query Parameter.Value --output text)"

echo "Account: $AWS_ACCOUNT_ID | Cluster: $CLUSTER_NAME | Region: $AWS_DEFAULT_REGION"

Double-check that CLUSTER_NAME matches your existing EKS cluster exactly, and that your default AWS CLI region matches where that cluster lives. A mismatched region here is the second most common source of confusing errors in this whole process, right after the IAM issues covered in Step 3.

Step 2: Create the Node IAM Role for Karpenter-Launched Instances

Karpenter launches EC2 instances that need their own IAM role, separate from the role the Karpenter controller itself uses. This node role needs the standard EKS worker node policies plus SSM access for debugging.

cat << EOF > node-trust-policy.json
{
  "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://node-trust-policy.json

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

aws iam create-instance-profile \
  --instance-profile-name "KarpenterNodeInstanceProfile-${CLUSTER_NAME}"

aws iam add-role-to-instance-profile \
  --instance-profile-name "KarpenterNodeInstanceProfile-${CLUSTER_NAME}" \
  --role-name "KarpenterNodeRole-${CLUSTER_NAME}"

Map this new role into your cluster’s aws-auth ConfigMap, or the equivalent access entry if you’re using EKS access entries instead of the legacy ConfigMap, so nodes launched under this role can actually join the cluster. Skipping this step is the number one reason Karpenter appears to “launch instances that never join the cluster.”

Step 3: Create the Controller IAM Role With Pod Identity

The Karpenter controller pod needs its own IAM role to call the EC2 Fleet, EC2, IAM PassRole, and SQS APIs. Most 2026 deployments use EKS Pod Identity rather than the older IAM Roles for Service Accounts pattern, since Pod Identity removes the need to manage an OIDC provider association manually.

cat << EOF > controller-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "pods.eks.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}
EOF

aws iam create-role --role-name "KarpenterControllerRole-${CLUSTER_NAME}" \
  --assume-role-policy-document file://controller-trust-policy.json

aws eks create-pod-identity-association \
  --cluster-name "${CLUSTER_NAME}" \
  --namespace "${KARPENTER_NAMESPACE}" \
  --service-account karpenter \
  --role-arn "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}"

Attach an inline policy granting the controller permissions for EC2 describe and run and terminate actions, ec2:CreateFleet, iam:PassRole scoped to the node role from Step 2, eks:DescribeCluster, and full access to the SQS interruption queue you’ll create in Step 6. AWS publishes a maintained copy of this policy in the Karpenter Getting Started documentation, and pasting it in directly is safer than hand-writing it, since the required actions shift slightly between minor versions.

Step 4: Tag Subnets and Security Groups for Discovery

Karpenter doesn’t read your VPC configuration through EKS APIs alone. It discovers eligible subnets and security groups by tag, using the karpenter.sh/discovery key. If this tag is missing, Karpenter will accept a NodePool and EC2NodeClass without error, then simply never launch anything, which is a confusing silent failure for first-time users.

# Tag private subnets used by worker nodes
for subnet in $(aws eks describe-cluster --name "${CLUSTER_NAME}" \
  --query "cluster.resourcesVpcConfig.subnetIds" --output text); do
  aws ec2 create-tags --resources "$subnet" \
    --tags Key="karpenter.sh/discovery",Value="${CLUSTER_NAME}"
done

# Tag the cluster security group
CLUSTER_SG=$(aws eks describe-cluster --name "${CLUSTER_NAME}" \
  --query "cluster.resourcesVpcConfig.clusterSecurityGroupId" --output text)

aws ec2 create-tags --resources "$CLUSTER_SG" \
  --tags Key="karpenter.sh/discovery",Value="${CLUSTER_NAME}"

If your cluster uses separate security groups for different node groups rather than one shared cluster security group, tag every security group you want Karpenter-launched nodes to use. Double- and triple-check this step against your actual VPC layout in the console; a wrong subnet tag can silently place new nodes in a subnet with no route to your NAT gateway.

Step 5: Install Karpenter With Helm

With IAM and tagging in place, the Helm install itself is short. This pulls the official OCI chart from the ECR public gallery that AWS maintains for Karpenter releases.

helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version "${KARPENTER_VERSION}" \
  --namespace "${KARPENTER_NAMESPACE}" --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

Verify the controller pod is running before moving on:

kubectl get pods -n kube-system -l app.kubernetes.io/name=karpenter

# Expected output:
# NAME                         READY   STATUS    RESTARTS   AGE
# karpenter-7d8f9c6b5d-x2n4p   1/1     Running   0          45s
# karpenter-7d8f9c6b5d-k9m3q   1/1     Running   0          45s

Two replicas is the Helm chart default for leader-election high availability. If the pods show CrashLoopBackOff instead of Running, jump to the troubleshooting section below before proceeding, since the CRDs and NodePool won’t do anything useful with a broken controller.

Step 6: Create the SQS Interruption Queue and EventBridge Rules

Spot instances can be reclaimed by AWS with roughly two minutes of warning. Without an interruption queue wired up, Karpenter has no way to hear that warning, and pods on a reclaimed Spot node get killed ungracefully instead of being drained in advance.

aws sqs create-queue --queue-name "${CLUSTER_NAME}" \
  --attributes '{"MessageRetentionPeriod":"300","SqsManagedSseEnabled":"true"}'

QUEUE_ARN=$(aws sqs get-queue-attributes \
  --queue-url "https://sqs.${AWS_DEFAULT_REGION}.amazonaws.com/${AWS_ACCOUNT_ID}/${CLUSTER_NAME}" \
  --attribute-names QueueArn --query Attributes.QueueArn --output text)

for rule in SpotInterruption RebalanceRecommendation InstanceStateChange ScheduledChange; do
  aws events put-rule --name "Karpenter-${rule}-${CLUSTER_NAME}" \
    --event-pattern "{\"source\":[\"aws.ec2\"],\"detail-type\":[\"EC2 Spot Instance Interruption Warning\"]}"
  aws events put-targets --rule "Karpenter-${rule}-${CLUSTER_NAME}" \
    --targets "Id=1,Arn=${QUEUE_ARN}"
done

Each rule needs its own correct event pattern matching the specific event type (Spot interruption, rebalance recommendation, instance state change, and scheduled maintenance), so treat the snippet above as a starting skeleton and pull the exact four patterns from AWS’s current Karpenter Getting Started guide rather than reusing one pattern for all four rules.

Step 7: Define Your First EC2NodeClass

The EC2NodeClass CRD defines the AWS-specific side of node provisioning: which AMI to boot, which subnets and security groups to use, and which IAM instance profile to attach. Karpenter 1.x uses the stable v1 API for this resource, and as of Karpenter 1.0 the amiSelectorTerms field became mandatory, a change that breaks any tutorial or config you copy from a pre-1.0 source.

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  amiSelectorTerms:
    - alias: al2023@latest
  role: "KarpenterNodeRole-${CLUSTER_NAME}"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
  tags:
    karpenter.sh/discovery: "${CLUSTER_NAME}"

The alias: al2023@latest shorthand tells Karpenter to always resolve the newest Amazon Linux 2023 EKS-optimized AMI at launch time, which saves you from manually bumping AMI IDs every time AWS ships a patched image. Pin a specific version string instead of @latest if your organization requires AMI change control.

Step 8: Define a NodePool With a Spot and On-Demand Mix

The NodePool CRD is where scheduling intent lives: which instance families and sizes are eligible, whether to prefer Spot or On-Demand, and how aggressively to consolidate. This is the resource you’ll tune most often after the initial install.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["4"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h
  limits:
    cpu: 1000
    memory: 1000Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

Apply both manifests and confirm Karpenter picks them up:

kubectl apply -f ec2nodeclass.yaml
kubectl apply -f nodepool.yaml

kubectl get nodepool
# NAME      NODECLASS   NODES   READY   AGE
# default   default     0       True    12s

Zero nodes is the correct starting state. Karpenter provisions nothing until a pod actually needs somewhere to run, which is the entire point: no reserved idle capacity sitting around waiting for demand.

Step 9: Trigger Your First Provisioning Event

Deploy a workload that requests more resources than your existing nodes can fit, and watch Karpenter respond in real time.

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: karpenter-demo
spec:
  replicas: 10
  selector:
    matchLabels:
      app: karpenter-demo
  template:
    metadata:
      labels:
        app: karpenter-demo
    spec:
      containers:
        - name: nginx
          image: public.ecr.aws/nginx/nginx:latest
          resources:
            requests:
              cpu: "1"
              memory: 1Gi
EOF

kubectl logs -f -n kube-system -l app.kubernetes.io/name=karpenter | grep -i "launched node"

Within roughly 30 to 90 seconds you should see a new node registered:

kubectl get nodes -l karpenter.sh/nodepool=default

# NAME                          STATUS   ROLES    AGE   VERSION
# ip-10-0-42-201.ec2.internal   Ready       38s   v1.30.4-eks-...

Delete the demo deployment once you've confirmed provisioning works, then watch the node disappear on its own after the consolidateAfter window elapses. That's consolidation reclaiming the now-idle capacity without any manual scale-down command.

Step 10: Configure Disruption Budgets for Production Safety

Aggressive consolidation is great for cost, less great for a payment service that can't tolerate more than one node disappearing at a time. Disruption budgets let you cap how much churn Karpenter is allowed to cause, including time-based restrictions such as "no consolidation during business hours."

spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 5m
    budgets:
      - nodes: "10%"
      - nodes: "0"
        schedule: "0 9 * * mon-fri"
        duration: 8h

This example caps disruption to 10% of nodes at any given time, and blocks all voluntary disruption entirely during business hours on weekdays. PodDisruptionBudgets on your workloads still apply on top of this and can block consolidation independently, which is a frequent source of confusion covered in the pitfalls section below.

Step 11: Add a Second NodePool for GPU or Memory-Heavy Workloads

Most real clusters need more than one NodePool. A common pattern is a general-purpose default pool plus a dedicated pool for GPU inference workloads that should never land on cheaper CPU-only Spot instances.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-inference
spec:
  template:
    metadata:
      labels:
        workload-type: gpu
    spec:
      taints:
        - key: nvidia.com/gpu
          value: "true"
          effect: NoSchedule
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["g5", "g6"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 256
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30m

Note the taint paired with a toleration on the GPU workload's pod spec, and the switch from WhenEmptyOrUnderutilized to WhenEmpty, since you generally don't want Karpenter aggressively repacking expensive GPU instances that are actively running inference jobs.

Step 12: Monitor Karpenter With Metrics

Karpenter exposes Prometheus metrics on port 8080 by default, covering provisioning latency, node lifetime, and consolidation decisions. Scrape these with your existing Prometheus stack, or pair Karpenter with a dedicated cost-visibility tool if you want a dollar-denominated view of what consolidation is actually saving you month over month.

kubectl port-forward -n kube-system svc/karpenter 8080:8080 &
curl -s localhost:8080/metrics | grep karpenter_nodeclaims_terminated_total

The key metrics to alert on are karpenter_provisioner_scheduling_duration_seconds (how long provisioning decisions take), karpenter_nodes_terminated_total broken down by termination reason, and karpenter_interruption_actions_performed_total to confirm Spot interruption handling is actually firing rather than silently failing.

Sizing Your First NodePool: A Worked Example

The default NodePool from Step 8 is intentionally broad, which is the right starting point, but production teams usually want to reason about what it will actually cost before they let it loose on a cluster. Take a mid-size API workload requesting 2 vCPU and 4Gi of memory per pod, running 40 replicas at typical load and bursting to 70 during peak hours. Under Cluster Autoscaler with a fixed m6i.xlarge node group, that workload needs roughly 20 nodes at typical load and 35 at peak, each billed at full On-Demand price regardless of how tightly the pods actually pack.

Point Karpenter at the same workload with a NodePool allowing the c, m, and r instance categories across generations 5 and up, and capacity-type set to both spot and on-demand, and the picture changes. Karpenter will typically bin-pack the 40 baseline pods onto a mix of 8 to 12 larger instances rather than 20 fixed-size ones, and it will fill peak demand with Spot capacity from whichever of the eligible families has the deepest availability at that moment, rather than being locked into m6i specifically. The table below shows how that plays out across three common NodePool configurations, using approximate US East pricing as of August 2026.

NodePool ConfigurationCapacity TypeEst. Nodes at PeakRelative Monthly Cost
Fixed m6i.xlarge node group (Cluster Autoscaler)On-Demand only35Baseline (100%)
Karpenter, on-demand only, c/m/r categoriesOn-Demand only22~72% of baseline
Karpenter, mixed spot/on-demand, c/m/r categoriesSpot + On-Demand20~41% of baseline

These figures are illustrative, not a guarantee for your workload, since actual savings depend heavily on your pods' real resource requests versus their limits, how tolerant your application is to Spot interruption, and which instance families have Spot capacity in your specific Availability Zones on any given day. Treat the table as a planning exercise to run through before your first production NodePool, not as a number to quote to a finance team without validating it against your own CloudWatch and Cost Explorer data first.

Karpenter vs Cluster Autoscaler: Feature Comparison

CapabilityKarpenter 1.13.0Cluster Autoscaler
Provisioning unitIndividual EC2 instances via Fleet APIPre-defined node groups / ASGs
Instance type selectionDynamic, dozens of eligible types per NodePoolFixed per node group
Bin-packing / consolidationContinuous, automatic repackingScale-down of empty nodes only
Spot handlingNative, with SQS/EventBridge interruption queueRequires separate tooling (e.g. AWS Node Termination Handler)
Provisioning latencyTypically 30 to 90 secondsOften 3 to 5 minutes (ASG launch cycle)
Reported cost savings40-60% (Prodigy), 57% (Armakuni), 5%+ (Salesforce)Varies; generally lower without manual tuning
Config surfaceKubernetes-native CRDs (NodePool, EC2NodeClass)ASG configuration plus deployment flags

5 Common Pitfalls When Setting Up Karpenter

1. Node role and controller role confusion. These are two separate IAM roles with two separate trust policies. Attaching the controller's permissions to the node role, or vice versa, is the most common setup mistake, and it produces error messages that don't obviously point back to the IAM misconfiguration.

2. Missing or mismatched discovery tags. If subnets, security groups, or the node role itself aren't tagged with the exact karpenter.sh/discovery value matching your cluster name, Karpenter accepts your NodePool with no error and simply never launches a node. There's no obvious log line pointing at "your tags are wrong," so you have to know to check.

3. Forgetting the aws-auth mapping for the node role. Nodes launch, appear healthy in the EC2 console, and then never join the Kubernetes cluster because the IAM role that launched them was never mapped to a Kubernetes RBAC identity.

4. Overly restrictive PodDisruptionBudgets blocking consolidation. A PDB requiring 100% of replicas available at all times will silently prevent Karpenter, and Cluster Autoscaler, from ever safely draining a node. If nodes never seem to consolidate despite low utilization, check your PDBs before checking Karpenter's config.

5. Copying pre-1.0 YAML examples. A large amount of Karpenter documentation and blog content online predates the 1.0 API stabilization. Pre-1.0 examples use karpenter.sh/provisioner-name instead of NodePools, and omit the now-mandatory amiSelectorTerms field entirely. Applying old YAML against a 1.13.0 install produces CRD validation errors that are easy to misread as a bug rather than a version mismatch.

Troubleshooting: 8 Issues and Fixes

Controller pods stuck in CrashLoopBackOff. Run kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter --previous. Nine times out of ten this traces back to the Pod Identity association or IRSA trust policy not matching the service account name exactly (it must be karpenter in the kube-system namespace unless you changed the Helm values).

NodePool created but zero nodes ever launch, even under pending pod pressure. Check discovery tags on subnets and security groups first. Then check that your NodePool's requirements aren't accidentally impossible to satisfy, for example requiring an instance generation greater than a number that excludes every instance family available in your region.

Nodes launch but never reach Ready. This is almost always the aws-auth ConfigMap or access entry missing the node IAM role, or a security group blocking the node's outbound connection to the EKS control plane API endpoint.

"unable to resolve AMI" error in controller logs. Your amiSelectorTerms alias doesn't match a valid AMI family, or a custom AMI filter returns zero results. Confirm with aws ssm get-parameter against the same SSM parameter path Karpenter uses internally.

Spot instances terminate without a graceful drain. The interruption queue isn't wired up, or the EventBridge rule's event pattern doesn't match the actual event AWS is sending. Verify with aws sqs receive-message against your queue during a simulated interruption test.

Consolidation never triggers despite obviously idle nodes. Check PodDisruptionBudgets first, then check for pods with no controller reference (bare pods aren't safely evictable by Karpenter by default), then check the disruption budgets you may have set too conservatively in Step 10.

"nodeClassRef not found" validation error on apply. The NodePool references an EC2NodeClass name that doesn't exist yet, or was created under a different API group during a version upgrade. Confirm with kubectl get ec2nodeclass that the referenced name is present and spelled identically.

Helm upgrade fails with CRD conflicts. Karpenter's CRDs are managed outside the main Helm release in some install patterns. If you're upgrading from an older Karpenter version, apply the new CRD manifests separately before running helm upgrade, since Helm does not automatically update CRDs on upgrade by default.

Costs go up instead of down after installing Karpenter. Check your NodePool's instance-category and instance-generation requirements. An overly narrow set of eligible instance types defeats the entire cost advantage, since Karpenter can only pick the cheapest fit from whatever list you gave it.

Advanced Tips for Running Karpenter in Production

Once the base setup is stable, a few refinements make a real difference at scale. First, split NodePools by weight rather than by hard exclusion when you want to bias toward Spot without fully banning On-Demand. Set a higher weight field on the Spot-preferring NodePool so Karpenter tries it first and only falls back to the On-Demand pool if Spot capacity isn't available. Second, use karpenter.k8s.aws/instance-generation: Gt "4" style requirements to exclude aging instance generations that are both slower and, counterintuitively, sometimes not meaningfully cheaper than newer generations, since Spot pricing reflects current supply and demand rather than instance age.

Third, if you're running Karpenter across multiple clusters in the same account, give each cluster's interruption queue and discovery tag a genuinely unique value tied to the cluster name, not a shared value. Cross-cluster tag collisions have caused nodes from one cluster to be discoverable, and in rare misconfigurations claimable, by another cluster's Karpenter controller. Fourth, set expireAfter on every NodePool even in non-Spot pools. Forcing periodic node replacement means you're never running a node so old it missed several AMI security patches, and it creates a natural, low-drama cadence for AMI rollout instead of a manual fleet-wide replacement event.

Complete Working Project: Karpenter on a Fresh EKS Cluster

The following sequence strings together every step above into a single repeatable path, assuming you're starting from an EKS cluster created via eksctl with a small managed node group to host the Karpenter controller itself.

# 1. Create a minimal EKS cluster to host the Karpenter controller
eksctl create cluster --name "${CLUSTER_NAME}" \
  --region "${AWS_DEFAULT_REGION}" \
  --version "${K8S_VERSION}" \
  --nodegroup-name karpenter-host \
  --node-type m6i.large \
  --nodes 2 --nodes-min 2 --nodes-max 2 \
  --with-oidc

# 2. Run Steps 2-6 above (node role, controller role, tagging, SQS/EventBridge)

# 3. Install Karpenter (Step 5)
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version "${KARPENTER_VERSION}" --namespace kube-system --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" --wait

# 4. Apply EC2NodeClass and NodePool (Steps 7-8)
kubectl apply -f ec2nodeclass.yaml
kubectl apply -f nodepool.yaml

# 5. Validate with a scaling test (Step 9)
kubectl create deployment karpenter-demo --image=public.ecr.aws/nginx/nginx:latest --replicas=10
kubectl get nodes -l karpenter.sh/nodepool=default --watch

From here, the project is production-ready enough to layer in the disruption budgets from Step 10, a second GPU or memory-optimized NodePool from Step 11, and Prometheus scraping from Step 12. Total setup time for an experienced Kubernetes operator running this end to end is roughly 90 minutes, most of which is IAM role creation and verification rather than Kubernetes YAML.

Karpenter Version and EKS Compatibility Reference

Karpenter VersionMinimum EKS VersionAPI VersionNotes
1.13.0 (current, June 2026)1.30+karpenter.sh/v1, karpenter.k8s.aws/v1Stable v1 CRDs, mandatory amiSelectorTerms
1.0.x1.28+karpenter.sh/v1First stable v1 API release, amiSelectorTerms became required
0.34.x1.29+karpenter.sh/v1beta1Added spot-to-spot consolidation feature gate
0.3x.x (legacy)1.23+karpenter.sh/v1alpha5Provisioner CRD, deprecated and replaced by NodePool

Running an alpha or beta-era Karpenter install against a cluster you've since upgraded past its originally supported Kubernetes version is a real and common source of subtle breakage. If you inherited a cluster with an old Karpenter install, check the CRD API version with kubectl get crd nodepools.karpenter.sh -o jsonpath='{.spec.versions[*].name}' before assuming any YAML you find online will apply cleanly.

For teams evaluating Karpenter against other autoscaling approaches before committing engineering time, AWS's own EKS documentation and the official Karpenter docs are the two sources worth reading end to end, since both are updated on every minor release, while community blog content lags behind by months at a time.

How Karpenter Fits Into a Broader FinOps Strategy

Karpenter solves the provisioning half of Kubernetes cost management, but it doesn't solve visibility. Teams that install Karpenter and stop there often find they've traded "too many idle nodes" for "the right number of nodes, but no idea which team or workload is driving the bill." Pairing Karpenter with a cost allocation tool that reads Kubernetes labels and namespaces gives you the other half of the picture: which deployment's resource requests are actually driving Karpenter's instance selection, and whether a given team's workloads could tolerate a smaller NodePool.

This matters because Karpenter's cost benefit is proportional to how accurately your pods declare their resource requests. A pod requesting 4 CPU when it only uses 0.5 CPU under normal load still forces Karpenter to provision for 4 CPU, since the scheduler and Karpenter both work off declared requests, not observed usage. Vertical Pod Autoscaler in recommendation-only mode, run alongside Karpenter rather than in its automatic-resizing mode, is a common pairing for finding and fixing those oversized requests before they inflate every node Karpenter launches.

Migrating an Existing Cluster From Cluster Autoscaler to Karpenter

Do not run both autoscalers against the same node group at the same time. The cleanest migration path is to leave Cluster Autoscaler managing your existing node groups untouched, install Karpenter alongside it targeting a brand-new NodePool, and then gradually cordon and drain nodes from the old node groups so their pods reschedule onto Karpenter-provisioned nodes. Once a node group's node count naturally reaches zero, delete the node group and remove Cluster Autoscaler's IAM permissions and deployment for that group.

Keep one small, static node group outside of both autoscalers for critical cluster add-ons such as CoreDNS and the Karpenter controller itself. Letting the controller depend on the nodes it manages creates a chicken-and-egg failure mode: if all Karpenter-managed nodes disappear during an incident, there's no capacity left for Karpenter itself to recover and provision replacements.

Security Considerations Specific to Karpenter

Because Karpenter's controller has permission to launch and terminate EC2 instances and to pass an IAM role to those instances, its controller role deserves the same scrutiny you'd give any identity with broad EC2 permissions in the account. Scope the iam:PassRole permission tightly to the specific node role ARN created in Step 2, rather than granting a wildcard PassRole permission across all roles in the account. A wildcard here means a compromised or misconfigured Karpenter controller could pass a far more privileged role to a launched instance than the node role you intended, which turns a scaling bug into a privilege escalation path.

It's also worth auditing which subnets carry the karpenter.sh/discovery tag on a recurring basis, particularly in accounts with multiple teams or multiple clusters. A subnet tagged for discovery by mistake, perhaps left over from a decommissioned cluster, can quietly become eligible for a different cluster's Karpenter controller to launch nodes into, placing workloads in network segments they were never designed to run in. Treat the discovery tag as a security boundary, not just a convenience label, and remove it as part of your cluster teardown process rather than leaving it behind.

Frequently Asked Questions

Is Karpenter free to use?
Yes. Karpenter is an open source project under the Apache 2.0 license, maintained jointly by AWS and the broader Kubernetes community. You pay only for the EC2 instances, EBS volumes, and other AWS resources it provisions, plus the negligible SQS and EventBridge costs from the interruption handling pipeline.

Can Karpenter run outside of AWS?
The core Karpenter project (kubernetes-sigs/karpenter) is cloud-agnostic at the API level, and the AWS-specific implementation lives in the separate aws/karpenter-provider-aws repository this tutorial covers. Other providers have their own Karpenter-style implementations in progress, but they are not drop-in replacements for the AWS provider's CRDs.

Do I still need Cluster Autoscaler if I install Karpenter?
No. Karpenter replaces Cluster Autoscaler's function entirely for the node groups it manages. Running both against the same node pool creates a scaling conflict where each tool tries to correct for the other's decisions, so most teams migrate node groups to Karpenter incrementally, as described above, and remove Cluster Autoscaler from each group once Karpenter takes over.

What happens to a Karpenter-launched node if the Karpenter controller crashes?
Existing nodes and their running pods are unaffected, since Karpenter's role ends once a node is registered and Ready. New pending pods simply won't get new nodes provisioned until the controller recovers, and consolidation pauses until it's back.

How does Karpenter decide between Spot and On-Demand?
Based on the karpenter.sh/capacity-type requirement in your NodePool and, when both are allowed, based on price and current Spot capacity availability across all eligible instance types, preferring the cheapest available option that satisfies the pending pod's requirements.

Can I use Karpenter with Fargate?
Karpenter provisions EC2 instances, not Fargate capacity, so the two are complementary rather than overlapping. A common pattern runs system-critical pods like the Karpenter controller itself on a Fargate profile or a small static node group, while Karpenter handles everything else on EC2.

Does upgrading Karpenter require downtime?
No, if you follow the documented upgrade path of applying new CRDs before running helm upgrade. The controller runs with two replicas by default for leader-election continuity, so a rolling controller upgrade doesn't disrupt already-running nodes or workloads.

Why did my NodePool stop launching new instances after working fine for weeks?
Check the limits field on your NodePool first. Karpenter enforces a hard CPU or memory ceiling per pool, and a growing cluster can silently hit that ceiling weeks after the initial setup, at which point new pods simply stay pending with no obvious error beyond a scheduling event you have to go looking for.

Related Coverage

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles