Skip to main content
This guide shows you how to switch from the Kubernetes Cluster Autoscaler (CAS) to Karpenter for automatic node provisioning. The migration is non-destructive: Karpenter is installed alongside CAS before CAS is disabled, so your workloads remain available throughout.

Assumptions

This guide assumes:
  • You have an existing EKS cluster with CAS installed
  • Your cluster uses existing VPC, subnets, and security groups
  • Your nodes are part of one or more managed node groups
  • Your workloads have pod disruption budgets that follow EKS best practices
  • Your cluster has an OIDC provider configured for service accounts
  • The aws CLI is installed and configured

Key differences from Cluster Autoscaler

Before migrating, it helps to understand how the two systems differ conceptually:
Cluster AutoscalerKarpenter
Scaling modelScales existing Auto Scaling GroupsLaunches EC2 instances directly via RunInstances
Instance selectionFixed per node groupDynamically chosen per workload requirement
ConfigurationOne node group per instance type setOne NodePool covers many instance types and zones
Node lifecycleASG manages nodesKarpenter manages the full instance lifecycle
Spot supportSeparate Spot node groupsNative karpenter.sh/capacity-type requirement
ConsolidationLimited (scale down)Continuous bin-packing and right-sizing
With Karpenter you will replace your ASG-based node groups (for workload capacity) with one or a few NodePool and EC2NodeClass objects. Your existing managed node groups can be kept at a minimal size to host Karpenter itself and other critical cluster components.

Migrate to Karpenter

1

Set environment variables

Set your cluster name and collect the variables needed throughout this guide:
KARPENTER_NAMESPACE=kube-system
CLUSTER_NAME=<your cluster name>
AWS_PARTITION="aws" # use aws-cn or aws-us-gov for non-standard partitions
AWS_REGION="$(aws configure list | grep region | tr -s " " | cut -d" " -f3)"
OIDC_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" \
    --query "cluster.identity.oidc.issuer" --output text)"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
K8S_VERSION=$(aws eks describe-cluster --name "${CLUSTER_NAME}" \
    --query "cluster.version" --output text)
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 | xargs aws ec2 describe-images \
    --query 'Images[0].Name' --image-ids | sed -r 's/^.*(v[[:digit:]]+).*$/\1/')"
2

Create the Karpenter node IAM role

Nodes launched by Karpenter need their own IAM role with the standard EKS worker-node policies. Create it with a trust policy that allows EC2 to assume the role:
echo '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}' > node-trust-policy.json

aws iam create-role --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --assume-role-policy-document file://node-trust-policy.json
Attach the required managed policies:
aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKSWorkerNodePolicy"

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKS_CNI_Policy"

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly"

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonSSMManagedInstanceCore"
3

Create the Karpenter controller IAM role

The Karpenter controller uses IAM Roles for Service Accounts (IRSA) to call AWS APIs. Create a role with a trust policy that allows your cluster’s OIDC provider to issue credentials to the Karpenter service account:
cat << EOF > controller-trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_ENDPOINT#*//}"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "${OIDC_ENDPOINT#*//}:aud": "sts.amazonaws.com",
                    "${OIDC_ENDPOINT#*//}:sub": "system:serviceaccount:${KARPENTER_NAMESPACE}:karpenter"
                }
            }
        }
    ]
}
EOF

aws iam create-role --role-name "KarpenterControllerRole-${CLUSTER_NAME}" \
    --assume-role-policy-document file://controller-trust-policy.json
Create and attach the controller policy granting Karpenter the permissions it needs to manage EC2 instances, IAM instance profiles, and SQS interruption queues:
cat << EOF > controller-policy.json
{
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter",
                "ec2:DescribeImages",
                "ec2:RunInstances",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceTypes",
                "ec2:DescribeInstanceTypeOfferings",
                "ec2:DeleteLaunchTemplate",
                "ec2:CreateTags",
                "ec2:CreateLaunchTemplate",
                "ec2:CreateFleet",
                "ec2:DescribeSpotPriceHistory",
                "pricing:GetProducts"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "Karpenter"
        },
        {
            "Action": "ec2:TerminateInstances",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/karpenter.sh/nodepool": "*"
                }
            },
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "ConditionalEC2Termination"
        },
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}",
            "Sid": "PassNodeIAMRole"
        },
        {
            "Effect": "Allow",
            "Action": "eks:DescribeCluster",
            "Resource": "arn:${AWS_PARTITION}:eks:${AWS_REGION}:${AWS_ACCOUNT_ID}:cluster/${CLUSTER_NAME}",
            "Sid": "EKSClusterEndpointLookup"
        },
        {
            "Sid": "AllowScopedInstanceProfileCreationActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": ["iam:CreateInstanceProfile"],
            "Condition": {
                "StringEquals": {
                    "aws:RequestTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
                    "aws:RequestTag/topology.kubernetes.io/region": "${AWS_REGION}"
                },
                "StringLike": {
                    "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*"
                }
            }
        },
        {
            "Sid": "AllowInstanceProfileReadActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": "iam:GetInstanceProfile"
        },
        {
            "Sid": "AllowUnscopedInstanceProfileListAction",
            "Effect": "Allow",
            "Resource": "*",
            "Action": "iam:ListInstanceProfiles"
        }
    ],
    "Version": "2012-10-17"
}
EOF

aws iam put-role-policy --role-name "KarpenterControllerRole-${CLUSTER_NAME}" \
    --policy-name "KarpenterControllerPolicy-${CLUSTER_NAME}" \
    --policy-document file://controller-policy.json
4

Tag subnets and security groups

Karpenter uses tag-based discovery to find which subnets and security groups to use when launching nodes. Tag the subnets for all your node groups:
for NODEGROUP in $(aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" \
    --query 'nodegroups' --output text); do
    aws ec2 create-tags \
        --tags "Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}" \
        --resources $(aws eks describe-nodegroup --cluster-name "${CLUSTER_NAME}" \
        --nodegroup-name "${NODEGROUP}" --query 'nodegroup.subnets' --output text)
done
Tag the security groups. The commands below cover two common EKS configurations — use the one that matches your setup:
NODEGROUP=$(aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" \
    --query 'nodegroups[0]' --output text)

SECURITY_GROUPS=$(aws eks describe-cluster \
    --name "${CLUSTER_NAME}" \
    --query "cluster.resourcesVpcConfig.clusterSecurityGroupId" \
    --output text)

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

Update the aws-auth ConfigMap

Allow nodes using the new Karpenter node IAM role to join the cluster by adding a mapping to the aws-auth ConfigMap:
kubectl edit configmap aws-auth -n kube-system
Add the following entry to the mapRoles section. Replace ${AWS_PARTITION} and ${AWS_ACCOUNT_ID} with your actual values, but do not replace {{EC2PrivateDNSName}}:
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}
  username: system:node:{{EC2PrivateDNSName}}
The aws-auth ConfigMap should now contain two role mappings: one for your existing node group and one for the new Karpenter node role.
6

Deploy Karpenter

Set the Karpenter version to deploy:
export KARPENTER_VERSION="1.9.0"
Generate the Karpenter manifest from the Helm chart. This approach lets you inspect and edit the manifest before applying it:
helm template karpenter oci://public.ecr.aws/karpenter/karpenter \
    --version "${KARPENTER_VERSION}" \
    --namespace "${KARPENTER_NAMESPACE}" \
    --set "settings.clusterName=${CLUSTER_NAME}" \
    --set "settings.interruptionQueue=${CLUSTER_NAME}" \
    --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${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 > karpenter.yaml
Before applying, edit karpenter.yaml to set node affinity so Karpenter runs on your existing managed node group rather than on a Karpenter-provisioned node. Find the Karpenter Deployment and update its affinity:
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: karpenter.sh/nodepool
          operator: DoesNotExist
        - key: eks.amazonaws.com/nodegroup
          operator: In
          values:
          - ${NODEGROUP}
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: "kubernetes.io/hostname"
If you have critical cluster add-ons like CoreDNS or metrics-server that you want to keep on managed node group nodes during the transition, set a similar nodeAffinity on those deployments too:
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: eks.amazonaws.com/nodegroup
          operator: In
          values:
          - ${NODEGROUP}
Now install the CRDs and apply the Karpenter manifest:
kubectl create namespace "${KARPENTER_NAMESPACE}" || true
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodepools.yaml"
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml"
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodeclaims.yaml"
kubectl apply -f karpenter.yaml
7

Create a default NodePool

Create a NodePool that covers the workload requirements previously handled by your CAS-managed node groups. The example below requests Spot capacity across the c, m, and r instance families — adjust the requirements to match your workloads.You can find additional NodePool examples at github.com/aws/karpenter/tree/v1.9.0/examples/v1.
cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h # 30 * 24h = 720h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-${CLUSTER_NAME}"
  amiSelectorTerms:
    - alias: "al2023@${ALIAS_VERSION}"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
EOF
8

Disable Cluster Autoscaler

With Karpenter running, scale the Cluster Autoscaler to zero replicas:
kubectl scale deploy/cluster-autoscaler -n kube-system --replicas=0
Then reduce your managed node groups to a minimal size. Karpenter will take over provisioning capacity for your workloads. Keeping a small number of managed node group nodes ensures Karpenter itself and other critical components have a stable place to run.
If your workloads do not have pod disruption budgets configured, scaling down node groups will cause workload disruption.
aws eks update-nodegroup-config --cluster-name "${CLUSTER_NAME}" \
    --nodegroup-name "${NODEGROUP}" \
    --scaling-config "minSize=2,maxSize=2,desiredSize=2"
If you have a large number of nodes, scale down gradually — a few instances at a time — and watch for workloads that might not have enough replicas or disruption budgets configured.
9

Validate the migration

As managed node group nodes are drained, verify that Karpenter is provisioning new nodes to replace them:
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller
Watch for new Karpenter-managed nodes appearing in the cluster:
kubectl get nodes
Karpenter-managed nodes will have the label karpenter.sh/nodepool set to the name of the NodePool that provisioned them. You can filter for them with:
kubectl get nodes -l karpenter.sh/nodepool

Mapping CAS node groups to NodePools

With CAS you typically created one node group per combination of instance type, availability zone, and capacity type (on-demand vs Spot). Karpenter collapses this into a single NodePool with a requirements block that expresses the same constraints declaratively. For example, a CAS setup with separate on-demand and Spot node groups across three instance types and three availability zones (18 node groups) maps to a single NodePool:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["us-west-2a", "us-west-2b", "us-west-2c"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
Karpenter will automatically select the best-fitting instance type and zone for each pending pod.

Next steps

NodePool concepts

Learn how to configure requirements, limits, disruption, and expiry.

Scheduling

Control which NodePool provisions your workloads using node selectors and affinity.

Disruption

Understand consolidation, expiry, drift, and interruption handling.

Troubleshooting

Diagnose common Karpenter issues.

Build docs developers (and LLMs) love