{"slug": "how-to-migrate-from-cluster-autoscaler-to-karpenter", "title": "How to Migrate from Cluster Autoscaler to Karpenter", "summary": "According to the Cast AI 2026 State of Kubernetes Optimization report, which analyzed 23,000+ clusters, the median CPU utilization across autoscaled EKS clusters is 8%, with 69% of Kubernetes clusters over-provisioning CPU. Cast AI's guide details how to migrate from Cluster Autoscaler to Karpenter, which provisions nodes in 45-90 seconds versus 3-5 minutes for Cluster Autoscaler, and supports running both tools simultaneously during cutover with a rollback path at every step.", "body_md": "8% average CPU utilization across autoscaled EKS clusters. That is the median, per the Cast AI 2026 State of Kubernetes Optimization report covering 23,000+ clusters. Fixed node groups drive the gap: provisioned for worst-case instance size, not actual pod demand. Karpenter replaces that model with just-in-time provisioning, sizes nodes to actual resource requests, and consolidates continuously. This guide covers the full migration: install Karpenter alongside Cluster Autoscaler, map node groups to NodePools, shift workloads incrementally, then remove the old stack. A rollback path exists at every step.Key takeaways\n\n## Key takeaways\n\n- Karpenter provisions nodes in 45-90 seconds versus 3-5 minutes for Cluster Autoscaler (varies by AMI caching, region, and instance type; AL2023 with warm pools can provision faster; times reflect typical EKS deployment without prewarming)\n- You can run both tools simultaneously during cutover with proper workload isolation to prevent double-provisioning\n- Each node group maps to a NodePool plus EC2NodeClass pair\n- Rollback is possible at every step: keep Cluster Autoscaler installed throughout\n- Large enterprises including major SaaS companies have migrated to Karpenter at scale — AWS has documented multiple large-scale EKS migrations in their Architecture Blog\n\n## Why teams migrate from Cluster Autoscaler to Karpenter\n\nThe case for migrating comes down to three things: speed, flexibility, and waste reduction. Each has a direct production impact, and together they add up to a meaningful operational difference.\n\n### Speed\n\nKarpenter provisions new nodes in 45-90 seconds (varies by AMI caching, region, and instance type; AL2023 with warm pools can provision faster; times reflect typical EKS deployment without prewarming). Cluster Autoscaler typically takes 3-5 minutes. That gap compounds during traffic spikes when your cluster needs capacity quickly.\n\nCluster Autoscaler works by scaling existing Auto Scaling Groups. It polls the Kubernetes API, identifies pending pods, and triggers ASG scale-out. The EC2 instance then needs time to register and join the cluster. Karpenter bypasses the ASG layer entirely. Instead, it calls the EC2 API directly and watches pod events in real time.\n\n### Flexibility\n\nCluster Autoscaler requires a separate node group for each combination of instance type and capacity type. Managing multiple groups with overlapping configurations adds operational overhead. Karpenter handles spot and on-demand in a single NodePool and selects across hundreds of instance types based on availability and cost.\n\nFor a full comparison of both approaches and their trade-offs, see [Karpenter vs Cluster Autoscaler](https://cast.ai/blog/karpenter-vs-cluster-autoscaler/).\n\n### Waste reduction\n\nThe Cast AI 2026 State of Kubernetes Optimization report, based on data from 23,000+ clusters, found that 69% of Kubernetes clusters over-provision CPU. Average CPU utilization across autoscaled clusters sits at 8%. Fixed node groups contribute to this problem because you provision for the largest expected node size, not for actual pod bin-packing needs.\n\nKarpenter’s consolidation policy actively removes underutilized nodes and repacks pods onto fewer, better-sized instances. This reduces the idle capacity you would otherwise pay for. For background on how Cluster Autoscaler handles scaling before you compare the two, see the [Cluster Autoscaler guide](https://cast.ai/blog/kubernetes-cluster-autoscaler/).\n\n## Prerequisites\n\nBefore starting the migration, confirm you have everything below. Missing any one item causes silent provisioning failures that are frustrating to debug.\n\n### EKS cluster requirements\n\n- EKS cluster running Kubernetes 1.27 or later\n- AWS CLI and kubectl configured with appropriate permissions\n- Helm 3.x installed locally\n\n### IAM requirements\n\n- Karpenter controller IAM role using IRSA or EKS Pod Identity\n- Node IAM role:\n`KarpenterNodeRole-${CLUSTER_NAME}`\n\n- Required managed policies: AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryReadOnly, AmazonSSMManagedInstanceCore\n\n### Infrastructure tagging\n\nTag your VPC subnets and security groups before installing Karpenter. Karpenter uses these tags to auto-discover networking resources through your EC2NodeClass configuration.\n\n```\nkarpenter.sh/discovery: ${CLUSTER_NAME}\n```\n\nAlso check whether existing node groups use custom AMIs, GPU instances, or arm64 workloads. Each of these requires explicit configuration in your EC2NodeClass. For a broader introduction to how Karpenter selects instances and manages node lifecycles, see [What is Karpenter](https://cast.ai/blog/what-is-karpenter/).\n\n## Step by step: migrating from Cluster Autoscaler to Karpenter\n\n### Install Karpenter\n\n**Parallel-run isolation required.** Cluster Autoscaler and Karpenter do not conflict by design. CA only manages nodes in ASGs it is explicitly configured to watch. Karpenter calls the EC2 API directly and registers nodes outside any CA-managed ASG, so CA cannot scale down or manage Karpenter nodes.\n\nThe real isolation risk is at the pod level: pending pods without explicit routing may be scheduled to either autoscaler’s capacity. To guarantee which autoscaler handles which workloads:\n\n**Option A — Taints + Tolerations (recommended):** Add a taint to your NodePool template:\n\n```\nspec:\n  template:\n    spec:\n      taints:\n        - key: karpenter-managed\n          value: \"true\"\n          effect: NoSchedule\n```\n\nThen add a toleration to pods that should run on Karpenter nodes:\n\n```\ntolerations:\n  - key: karpenter-managed\n    value: \"true\"\n    effect: NoSchedule\n```\n\nPods without this toleration will not schedule onto Karpenter nodes.\n\n**Option B — NodeSelector:** Add nodeSelector to your Karpenter NodePool template labels, then target that label in pod specs.\n\nInstall Karpenter using Helm without removing Cluster Autoscaler first. Cluster Autoscaler continues managing its existing node groups. Karpenter only provisions nodes for pods that match its NodePool selectors.\n\n```\nexport CLUSTER_NAME=<your-cluster>\nexport AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\nexport KARPENTER_VERSION=\"1.4.0\"  # Check https://github.com/aws/karpenter/releases for latest\n\nhelm registry logout public.ecr.aws || true\nhelm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \\\n  --version \"${KARPENTER_VERSION}\" \\\n  --namespace kube-system \\\n  --set \"settings.clusterName=${CLUSTER_NAME}\" \\\n  --set \"settings.interruptionQueue=${CLUSTER_NAME}\" \\\n  --set controller.resources.requests.cpu=1 \\\n  --set controller.resources.requests.memory=1Gi \\\n  --wait\n```\n\nVerify the controller pod is running before proceeding.\n\n```\nkubectl get pods -n kube-system -l app.kubernetes.io/name=karpenter\n```\n\nAnnotate the Karpenter controller deployment so Cluster Autoscaler does not evict its pods during the parallel-run phase:\n\n```\nkubectl patch deployment karpenter -n kube-system \\\n  -p '{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"cluster-autoscaler.kubernetes.io/safe-to-evict\": \"false\"}}}}}'\n```\n\nThis annotation tells Cluster Autoscaler not to evict Karpenter controller pods when it considers scaling down a node. Without it, CA may disrupt the Karpenter controller during the parallel-run phase.\n\nSet `settings.interruptionQueue`\n\nto the name of your SQS queue for spot interruption handling. Without this, interrupted spot instances drain without graceful pod eviction.\n\n### Map node groups to NodePools\n\nThis is the core translation step. Each existing managed node group maps to a NodePool and EC2NodeClass pair. The NodePool defines what Karpenter can provision. The EC2NodeClass defines how it provisions those nodes.\n\n#### Cluster Autoscaler to Karpenter concept mapping\n\n| Cluster Autoscaler concept | Karpenter equivalent | Notes |\n|---|---|---|\n| Auto Scaling Group (ASG) | NodePool | Defines instance selection rules, capacity type, limits, disruption policy |\n| Launch Template / AMI | EC2NodeClass (amiSelectorTerms) | Use alias al2023@latest or specify custom AMIs |\n| Node group instance types (fixed) | NodePool requirements (instance-category, family, generation) | Karpenter selects optimally from hundreds of types |\n| Node group labels | NodePool template.metadata.labels | Applied to all provisioned nodes |\n| Node group taints | NodePool template.spec.taints | Pods must tolerate them |\n| ASG min/max size | NodePool spec.limits (cpu, memory) | Expressed as total resources, not node count |\n| Spot/On-demand (separate ASGs) | karpenter.sh/capacity-type: [spot, on-demand] | Native multi-type in one NodePool |\n| Subnet selection | EC2NodeClass subnetSelectorTerms | Tag subnets with karpenter.sh/discovery= ${CLUSTER_NAME} |\n| Security groups | EC2NodeClass securityGroupSelectorTerms | Tag SGs with karpenter.sh/discovery= ${CLUSTER_NAME} |\n| Node IAM role | EC2NodeClass spec.role | KarpenterNodeRole- ${CLUSTER_NAME} |\n\nASG min/max does not translate directly to NodePool limits. Karpenter counts total CPU and memory across all provisioned nodes, not node count. Set `spec.limits.cpu`\n\nbased on your peak resource demand, not a maximum node count.\n\nBelow are the NodePool and EC2NodeClass YAML examples for a general-purpose workload node group. For a full reference on all available NodePool fields, see [NodePool configuration](https://cast.ai/blog/karpenter-nodepools/).\n\n```\napiVersion: karpenter.sh/v1\nkind: NodePool\nmetadata:\n  name: default\nspec:\n  template:\n    spec:\n      requirements:\n        - key: kubernetes.io/arch\n          operator: In\n          values: [\"amd64\"]\n        - key: kubernetes.io/os\n          operator: In\n          values: [\"linux\"]\n        - key: karpenter.sh/capacity-type\n          operator: In\n          values: [\"spot\"]\n        - key: karpenter.k8s.aws/instance-category\n          operator: In\n          values: [\"c\", \"m\", \"r\"]\n        - key: karpenter.k8s.aws/instance-generation\n          operator: Gt\n          values: [\"2\"]\n      nodeClassRef:\n        group: karpenter.k8s.aws\n        kind: EC2NodeClass\n        name: default\n      expireAfter: 720h\n  limits:\n    cpu: 1000\n  disruption:\n    consolidationPolicy: WhenEmptyOrUnderutilized\n    consolidateAfter: 15m\napiVersion: karpenter.k8s.aws/v1\nkind: EC2NodeClass\nmetadata:\n  name: default\nspec:\n  role: \"KarpenterNodeRole-${CLUSTER_NAME}\"\n  amiSelectorTerms:\n    - alias: \"al2023@latest\"\n  subnetSelectorTerms:\n    - tags:\n        karpenter.sh/discovery: \"${CLUSTER_NAME}\"\n  securityGroupSelectorTerms:\n    - tags:\n        karpenter.sh/discovery: \"${CLUSTER_NAME}\"\n```\n\n### Shift workloads and verify\n\nStart with non-critical workloads. Add a label to your NodePool template, then use a matching `nodeSelector`\n\non the pods you want Karpenter to manage.\n\nIn the NodePool template metadata:\n\n```\ntemplate:\n  metadata:\n    labels:\n      provisioner: karpenter\n```\n\nOn the target deployment spec:\n\n```\nnodeSelector:\n  provisioner: karpenter\n```\n\nRedeploy the workload. Karpenter provisions a new node sized to the pod’s resource request. The old Cluster Autoscaler node stays until it becomes underutilized and scales down normally.\n\nWatch these signals during verification:\n\n`kubectl get nodeclaims`\n\n– confirms Karpenter provisioned nodes and their status`kubectl describe nodeclaim <name>`\n\n– shows instance selection and scheduling details- Pod events: confirm pods scheduled on Karpenter nodes, not Cluster Autoscaler nodes\n\nAfter shifting each workload batch, verify node and pod state before proceeding to the next batch:\n\n```\n# Verify no critical workloads remain on old nodes\nkubectl get pods -A -o wide | grep \n# Verify new Karpenter nodes are healthy\nkubectl get nodes -l karpenter.sh/nodepool\n# Verify pods are running on new nodes\nkubectl get pods -A -o wide | grep\n```\n\nRun batch-one workloads on Karpenter for 24-48 hours before expanding. Then progressively increase the set of workloads targeting Karpenter NodePools. Continue until all Cluster Autoscaler node groups are idle.\n\n### Remove old node groups\n\nBefore deleting a node group, confirm no pods still schedule there.\n\n```\nkubectl get pods -o wide | grep <node-name>\n```\n\nThen cordon and drain each node.\n\n```\nkubectl cordon <node-name>\nkubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data\n```\n\nAfter the nodes drain, delete the node group from the EKS console or via eksctl. Once all node groups are removed, uninstall Cluster Autoscaler.\n\n```\nhelm uninstall cluster-autoscaler -n kube-system\n```\n\n## Rollback and safety\n\nBecause Cluster Autoscaler stays installed throughout the migration, you have a fallback at every step. How you roll back depends on how far you have progressed.\n\nTo roll back a specific workload: remove the Karpenter `nodeSelector`\n\nfrom the deployment. Pods reschedule onto available nodes, including Cluster Autoscaler-managed ones if capacity exists.\n\nTo roll back completely, follow these steps in order:\n\n- Scale Cluster Autoscaler back up:\n`kubectl scale deployment cluster-autoscaler -n kube-system --replicas=1`\n\n- Delete Karpenter NodePools to trigger graceful node draining:\n`kubectl delete nodepool default`\n\n(Karpenter drains nodes before deleting them; pods are evicted and rescheduled automatically) - Allow the scheduler to reschedule pods onto CA-managed node groups as those scale up to meet demand\n- Verify all pods are running:\n`kubectl get pods -A | grep -v Running`\n\nKeep Cluster Autoscaler running for at least one week after you consider migration complete. This ensures you have observed the cluster under your full load pattern before removing the fallback.\n\n## Common migration pitfalls\n\nMost migration failures come from a small, repeatable set of mistakes. Here are the ones that appear most often.\n\n### Missing subnet and security group tags\n\nKarpenter cannot discover networking resources without the `karpenter.sh/discovery`\n\ntag. NodePool provisioning fails silently. Check Karpenter controller logs first for “no subnets found” or “no security groups found” errors. This single issue accounts for the majority of first-time installation failures.\n\n### Over-restrictive instance requirements\n\nPinning a NodePool to specific instance types recreates the same constraint you had with fixed node groups. Use `instance-category`\n\nand `instance-generation`\n\nrequirements instead. Karpenter then selects the most cost-effective available type that satisfies the pod’s resource request.\n\n### Missing node labels on NodePool templates\n\nLabels from node groups do not carry over to Karpenter nodes automatically. Define all required labels in `NodePool.spec.template.metadata.labels`\n\n. Also audit any PodAffinity or NodeSelector rules in your workloads. If they reference labels from old node groups, those rules will not match Karpenter-provisioned nodes.\n\n### Aggressive consolidation on stateful workloads\n\n`WhenEmptyOrUnderutilized`\n\nreplaces nodes frequently. This works well for stateless workloads but causes unnecessary disruption for stateful ones. For stateful workloads, switch to `WhenEmpty`\n\n. Also consider increasing the `consolidateAfter`\n\ninterval to give workloads time to stabilize before Karpenter triggers consolidation.\n\n### Missing PodDisruptionBudgets during cutover\n\nWhen draining nodes, pods without PDBs can be evicted simultaneously, causing downtime. Before cutover, verify every latency-sensitive deployment has a PDB:\n\n```\nkubectl get pdb -A\n```\n\nSet `minAvailable`\n\nor `maxUnavailable`\n\nto match your SLO requirements. Without PDBs, draining a node during cutover can evict all replicas of a deployment at once.\n\n## After migration: closing the CPU waste gap\n\nCompleting the migration moves node provisioning to Karpenter. Provisioning efficiency and resource efficiency are still separate problems.\n\nDespite Karpenter’s bin-packing, most clusters continue to over-provision CPU after migration. The root cause is inflated pod resource requests. When pods request more CPU and memory than they actually consume, Karpenter provisions nodes sized to those requests rather than actual usage. The provisioning model improves; the request accuracy problem remains.\n\nCast AI operates alongside Karpenter to close this gap:\n\n**Continuous workload rightsizing**: adjusts CPU and memory requests based on actual usage, so Karpenter bins pods against real consumption rather than padded estimates**Reliable Spot management**: predicts spot interruption events and proactively moves workloads before instances terminate, so you stay on Spot without the reliability risk**Zero-downtime container live migration (CRIU)**: moves stateful workloads across nodes without restarts, enabling consolidation that would otherwise be blocked by stateful pods\n\n## Frequently Asked Questions\n\n**How do I migrate from Cluster Autoscaler to Karpenter?** Install Karpenter alongside Cluster Autoscaler using Helm. Create a NodePool and EC2NodeClass for each existing node group. Migrate workloads incrementally using nodeSelector rules that target Karpenter-provisioned nodes. Remove Cluster Autoscaler and old node groups only after all workloads are stable on Karpenter nodes for at least 24-48 hours each.\n\n**Can I run both during migration?** Yes, with proper isolation. Cluster Autoscaler manages pods that schedule to its node groups. Karpenter manages pods that match its NodePool requirements. Without workload isolation, both controllers may provision nodes for the same pending pods. Use nodeSelector or taints to separate workloads during the transition period — and annotate the Karpenter controller pods with `cluster-autoscaler.kubernetes.io/safe-to-evict: \"false\"`\n\nto prevent CA from evicting them.\n\n**How do I map node groups to NodePools?** Each node group maps to one NodePool plus one EC2NodeClass. The NodePool defines instance requirements, capacity type (spot or on-demand), resource limits, and disruption policy. The EC2NodeClass defines AMI selection, subnets, security groups, and the node IAM role. The full mapping table in this guide lists every concept-to-concept translation.\n\n**What can go wrong during migration?** The most common issues are missing karpenter.sh/discovery tags on subnets and security groups, over-restrictive instance type requirements, a missing SQS interruption queue for spot nodes, missing PodDisruptionBudgets on latency-sensitive workloads, and node labels that do not carry over from old node groups. Check Karpenter controller logs early; they surface most of these problems as actionable error messages.\n\n**Is the migration reversible?** Yes, at every step. Cluster Autoscaler stays installed throughout the migration. To reverse a workload, remove its Karpenter nodeSelector. To reverse completely: scale Cluster Autoscaler back up (`kubectl scale deployment cluster-autoscaler -n kube-system --replicas=1`\n\n), delete Karpenter NodePools to trigger graceful draining (`kubectl delete nodepool default`\n\n), then verify all pods are running (`kubectl get pods -A | grep -v Running`\n\n). Pods reschedule onto CA node groups as those scale up.", "url": "https://wpnews.pro/news/how-to-migrate-from-cluster-autoscaler-to-karpenter", "canonical_source": "https://cast.ai/blog/cluster-autoscaler-to-karpenter-migration/", "published_at": "2026-08-05 10:42:44+00:00", "updated_at": "2026-08-05 10:54:02.246204+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "ai-infrastructure", "mlops", "developer-tools"], "entities": ["Cast AI", "Karpenter", "Cluster Autoscaler", "AWS", "EKS"], "alternates": {"html": "https://wpnews.pro/news/how-to-migrate-from-cluster-autoscaler-to-karpenter", "markdown": "https://wpnews.pro/news/how-to-migrate-from-cluster-autoscaler-to-karpenter.md", "text": "https://wpnews.pro/news/how-to-migrate-from-cluster-autoscaler-to-karpenter.txt", "jsonld": "https://wpnews.pro/news/how-to-migrate-from-cluster-autoscaler-to-karpenter.jsonld"}}