27.6 Karpenter: Next-Generation Node Autoscaler for EKS
Alright, let’s talk about Karpenter. Forget everything you thought you knew about autoscaling in Kubernetes, because this thing is a different beast entirely. The old Cluster Autoscaler (CAS) was like trying to parallel park a cruise ship—it worked, eventually, but it was slow, clunky, and you had to pre-define every single parking spot (node group) you might ever need. Karpenter is like teleportation. You say “I need a node with 4 CPUs and 16GB of RAM,” and it materializes the perfect instance for the job, often before the pod scheduler has even finished its cry for help. It’s not just scaling; it’s provisioning, and it does it with terrifying speed and efficiency.
How Karpenter Actually Works (The Magic, Demystified)
Karpenter’s core genius is its simplicity. You install a single Pod in your cluster (it’s its own controller), and then you give it IAM permissions to launch EC2 instances. That’s the basic setup. Its entire job is to watch the Kubernetes API for pods that can’t be scheduled—ones stuck in Pending because no existing node has the resources. When it sees one, it doesn’t fiddle around with modifying ASGs like CAS does. It directly calls the EC2 Fleet API with a shopping list of requirements: CPU, memory, architecture, maybe a GPU.
It then makes a brutally rational decision: “What is the cheapest, most available instance type that will fit this pod right now?” It doesn’t care about your pre-conceived notions of what an “app” node or a “worker” node should be. It’s a cold, calculating instance-buying robot, and I mean that as the highest compliment. It will launch a c6i.large, a m5zn.metal, or a r7g.2xlarge if that’s what makes the most economic and logistical sense at that exact second in that specific Availability Zone.
Installing the Ruthless Robot
Let’s get this thing running. First, you need to set up the IAM permissions and the Helm chart. Don’t worry, it’s not as bad as it sounds.
# Add the Karpenter Helm repository
helm repo add karpenter https://charts.karpenter.sh
helm repo update
# Create the IAM role and instance profile for the nodes Karpenter will launch
# This is typically done via AWS CloudFormation or Terraform. Let's be real, you're using Terraform.
# Here's a terrifically abbreviated version of the Terraform you'd need:
resource "aws_iam_instance_profile" "karpenter" {
name = "KarpenterNodeInstanceProfile-${var.cluster_name}"
role = aws_iam_role.karpenter_node.name
}
resource "aws_iam_role" "karpenter_node" {
name = "KarpenterNodeRole-${var.cluster_name}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
# You'd attach the standard AWS-managed policies for EKS worker nodes here
managed_policy_arns = [
"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
]
}
Now, install Karpenter itself. Notice we’re providing the ARN of the instance profile we just created so it knows what to launch.
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version v0.36.1 \
--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" \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
--wait
Your First Provisioner: Telling Karpenter the Rules
A Provisioner is your primary way of setting guardrails for Karpenter. It’s not a node group; think of it as a set of rules of engagement. “You can buy any instance type from this family, but nothing smaller than this, nothing bigger than that, and only use these subnets and these security groups.” Here’s a basic one:
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: default
spec:
# These are the limits of your sanity. Set them.
limits:
resources:
cpu: 1000 # Don't let this thing spend more than 1000 CPUs worth of money
memory: 1000Gi
providerRef:
name: default
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", "on-demand"] # It will aggressively prefer spot, because it's smart.
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.*", "c5.*", "r5.*"] # Constrain it to a sane family if you have specific needs
ttlSecondsAfterEmpty: 30 # This is the killer feature. Node has no pods for 30 seconds? Terminate it. Gone.
consolidation:
enabled: true # Allows it to replace nodes with cheaper ones or pack pods tighter to save money
The Pitfalls (Where This Brilliance Stubs Its Toe)
Karpenter is not without its sharp edges. First, the ttlSecondsAfterEmpty is glorious for cost savings but can be murder on services that need to do slow, expensive startup routines (I’m looking at you, Java). Your pod might come up on a node that gets terminated 30 seconds after the previous workload finished. Use ttlSecondsUntilEmpty for daemonsets or critical system nodes.
Second, its default provisioning is fast. Sometimes too fast. If you have a misconfigured deployment that suddenly requests 1000 replicas, Karpenter will gleefully and instantly launch $5000 worth of instances before you can blink. Those limits in the Provisioner aren’t a suggestion; they are a critical financial circuit breaker. Set them.
Finally, while it’s getting better, the tooling around custom AMIs and specialized node configurations (like those needing custom bootstrap.sh scripts) isn’t quite as point-and-click as managed node groups. You have to define it all in your Provider config, which is powerful but adds complexity.
The bottom line? Karpenter is the single biggest upgrade you can make to your EKS cluster’s efficiency and cost profile. It takes the rigid, cautious world of ASGs and replaces it with something that feels alive, responsive, and, frankly, a little bit clever. Just make sure you give it a very, very clear allowance.