Karpenter on EKS
Karpenter is a node autoscaler that launches EC2 instances directly, without Auto Scaling groups. It watches for unschedulable pods, works out the cheapest set of instances that satisfies their requirements, and launches them in well under a minute. It also removes and replaces nodes to keep the cluster cheap.
This page covers the AWS-specific side. For the scheduling model and how Karpenter compares in principle, see Karpenter (concepts) and Cluster Autoscaler (concepts).
EKS Auto Mode runs a managed Karpenter for you. If Auto Mode’s constraints are acceptable, you do not need to install or operate anything on this page.
What Karpenter needs in AWS
| Resource | Purpose |
|---|---|
| Controller role | Lets the controller call ec2:CreateFleet, RunInstances, TerminateInstances, read pricing and SSM |
| Node role and access entry | The IAM role instances assume; an access entry of type EC2_LINUX lets them join |
| Discovery tags | karpenter.sh/discovery: <cluster> on the subnets and security groups Karpenter may use |
| Interruption queue | An SQS queue fed by EventBridge rules for Spot interruptions, rebalance recommendations and scheduled maintenance |
The controller itself must not run on nodes it manages. Put it on a small managed node group or on Fargate.
The two objects you write
A NodePool says what kind of capacity is allowed and how it may be disrupted. An EC2NodeClass says how to build the instance.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
expireAfter: 720h
limits:
cpu: "1000"
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: "10%"
- nodes: "0"
schedule: "0 9 * * mon-fri"
duration: 8h
reasons: ["Drifted", "Underutilized"]
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@v20260901
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 80Gi
volumeType: gp3
encrypted: trueDesign notes:
- Keep requirements broad. The more instance types Karpenter may choose from, the cheaper and more available the result, especially on Spot. Constrain by category and generation rather than listing types.
- Pin the AMI.
alias: al2023@latestmeans every AMI release drifts and replaces every node. Pin a version and move it deliberately as part of an upgrade. - Always set
limits. A runaway Deployment otherwise scales the bill without bound. expireAfterrecycles nodes on a schedule, which is how you guarantee patch currency.
Disruption: the part that causes incidents
Karpenter voluntarily removes nodes for three reasons:
| Reason | Trigger |
|---|---|
| Consolidation | A node is empty, or its pods fit on other nodes or a cheaper replacement |
| Drift | The node no longer matches its NodePool or EC2NodeClass (new AMI, changed requirement) |
| Expiration | The node is older than expireAfter |
Disruption respects PodDisruptionBudgets and the karpenter.sh/do-not-disrupt: "true" pod annotation. Three rules keep it safe:
- Every service with more than one replica has a PDB. Without one, consolidation can drain all replicas at once.
- Use NodePool budgets to cap how many nodes are disrupted simultaneously and to block disruption during business hours.
- A PDB with
maxUnavailable: 0, or a single-replica workload withdo-not-disrupt, blocks the node forever. Expiration will eventually override it only ifterminationGracePeriodis set on the NodePool.
Spot
With both capacity types allowed, Karpenter prefers Spot and falls back to On-Demand when Spot is unavailable. It uses the price-capacity-optimized allocation strategy, so it picks pools that are cheap and unlikely to be reclaimed. When the interruption queue is configured, a two-minute Spot warning makes Karpenter cordon, drain and replace the node immediately. More in Spot on EKS.
To keep a baseline on On-Demand, run two NodePools with different weight values, or spread a workload across capacity types with a topology spread constraint on karpenter.sh/capacity-type.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Pods stay Pending, no NodeClaim created | No NodePool satisfies the pod’s node selector, affinity or tolerations; or the NodePool hit its limits |
| NodeClaim created, instance never joins | Node role has no access entry, or the node cannot reach the cluster endpoint (endpoint access) |
InsufficientInstanceCapacity in controller logs | Requirements too narrow for the zone; widen instance families |
| Nodes churn constantly | AMI alias set to latest, or consolidation fighting an HPA — raise consolidateAfter |
Pods Pending on IP exhaustion | Subnets are full — see prefix delegation and custom networking |
kubectl get nodepools,nodeclaims
kubectl describe nodeclaim <name>
kubectl logs -n kube-system deploy/karpenter -fRelated
- Compute options on EKS
- Autoscaling on EKS and cost optimization
- Graviton — mixing architectures in one NodePool
- EC2 cost optimization
- Karpenter documentation
Across the wiki
- AKS Autoscaling Architecture — Cluster Autoscaler, KEDA, and Virtual Nodes — autoscaling (Azure)
- GKE Autoscaling Architecture — Cluster Autoscaler, NAP, HPA v2, and VPA — autoscaling (GCP)
- Scalability — autoscaling (Architecture)