
Karpenter revolutionizes EKS scaling by replacing traditional Auto Scaling Groups with direct, pod-aware EC2 capacity provisioning. This guide explores how to implement it using Terraform for faster, more cost-effective cluster management.
Understanding How Karpenter Works
Traditional Kubernetes autoscaling on Amazon EKS is split into two loosely coupled components. The scheduler marks a pod as Pending when no existing node satisfies the podâs requests. A separate Cluster Autoscaler watches the Auto Scaling Groups (ASGs) that back the node groups, and when an ASG reports that its nodes are full it creates a new instance of the single instance type defined for that group. Because the ASG can only launch one predefined type, the autoscaler often adds capacity that is larger than required, and the loop can take several minutes as the ASG polls for capacity and the scheduler reâevaluates the pod placement.
Karpenter removes this twoâlayer indirection by collapsing the schedulerâautoscaler feedback loop into a single controller that talks directly to the EC2 Fleet API. When a pod remains Pending, Karpenter:
- Reads the podâs
resources.requests(CPU, memory),nodeSelectorconstraints (architecture, zone, GPU, etc.), and any customkarpenter.shlabels. - Queries the full catalog of EC2 instance types available in the target region.
- Selects the cheapest instance that satisfies all constraints, preferring Spot capacity when the
capacity-typelabel allows it. - Calls
ec2:CreateFleetto launch the instance immediately, bypassing the ASG provisioning cycle. - Registers the new node with the cluster; the pending pod is scheduled and becomes
Runningâtypically in under a minute.
Practical example: A workload requests 2 vCPU, 4âŻGiB memory, and arm64 architecture in the us-east-1a AZ. Karpenter evaluates the request, discovers that a t4g.large Spot instance meets the criteria at a lower price than any OnâDemand option, and launches it via the EC2 Fleet API. The node joins the cluster, the pod schedules, and the entire cycle completes in seconds.
On the scaleâdown path, Karpenter continuously monitors node utilization. If a node becomes empty or underâutilized, it consolidates workloads onto remaining nodes and terminates the surplus instance, again using direct EC2 API calls. This approach eliminates the need for multiple handâtuned node groups, reduces provisioning latency, and ensures that capacity is rightâsized for each podâs actual requirements.
Why Teams Choose Karpenter
Karpenter shifts the paradigm of cluster autoscaling by collapsing the traditional two-tier architectureâwhere a Kubernetes scheduler monitors pods and a separate Cluster Autoscaler manages Auto Scaling Groups (ASGs)âinto a single, direct control loop. By interfacing directly with the EC2 Fleet API, Karpenter bypasses the polling latency inherent in ASGs, enabling rapid infrastructure provisioning that aligns precisely with workload requirements.
Teams adopt Karpenter to move beyond the limitations of static node group management. Key architectural benefits include:
- Right-Sized Capacity: Rather than forcing workloads into pre-defined instance types, Karpenter evaluates the CPU, memory, architecture, and GPU constraints of pending pods. It then selects the most cost-effective instance type from a broad fleet, ensuring nodes are provisioned to match specific workload footprints.
- Faster Scaling: By eliminating the ASG polling loop, Karpenter reduces the latency between a pod becoming
Pendingand the node joining the cluster, frequently completing the provisioning cycle in under a minute. - Reduced Management Overhead: Karpenter replaces fragmented node group sprawl with a simplified model using
NodePoolsandEC2NodeClasses. This configuration eliminates the need to manually manage multiple node groups per instance family or Availability Zone. - Safe Spot Utilization: Karpenter mitigates the volatility of Spot instances through native interruption handling. It monitors SQS queues for AWS Spot termination notices, automatically cordoning and draining nodes before the instance is reclaimed, allowing teams to leverage significant cost savings without sacrificing workload availability.
- Automated Self-Healing: The infrastructure maintains itself through continuous consolidation and scheduled expiry. By setting
expireAfterpolicies, nodes are automatically replaced at defined intervals, which facilitates consistent AMI patching and prevents configuration drift.
Furthermore, Karpenterâs consolidation logic continuously re-evaluates cluster utilization. If workloads can be bin-packed more efficiently, the controller moves pods to underutilized nodes and terminates redundant capacity, ensuring that cluster costs remain optimized without manual intervention.
Implementing Cost-Effective Strategies
Before applying any costâsaving knobs, understand the two layers that traditionally drive node provisioning in Amazon EKS: the Kubernetes scheduler decides where a pod should run, while a separate Cluster Autoscaler watches Auto Scaling Groups (ASGs) and adds nodes. This split creates latency and forces operators to preâdefine a limited set of instance types. Karpenter collapses those layers into a single feedback loop: when a pod remains Pending, Karpenter evaluates the podâs exact resource, architecture, and zone requirements, then calls the EC2 Fleet API to launch the cheapest instance that satisfies them.
Below are the key configuration blocks that turn this capability into a repeatable, costâeffective strategy.
- Spotâfirst policy â Define the desired capacity types in the
NodePoolspec. Karpenter will prefer Spot instances and fall back to OnâDemand only when Spot capacity is unavailable.requirements: - key: karpenter.sh/capacity-type operator: In values: ["spot", "on-demand"] - Binâpacking consolidation â Enable the consolidation policy so that underâutilized nodes are drained and terminated, allowing workloads to be reâpacked onto fewer machines.
disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 1m - Broad instance selection â Instead of a single instance type, allow whole families (e.g.,
c,m,r). Karpenter then selects the cheapest specific type that meets the podâs constraints at launch time.instanceTypes: - "c*" - "m*" - "r*" - Scheduled node expiry for patching â Set a fixed lifetime for each node. When the timer expires, Karpenter replaces the node, automatically applying the latest AMI and eliminating drift.
expireAfter: 720h # 30 days - Graceful Spot interruption handling â Configure an EventBridge rule that forwards the twoâminute Spot interruption warning to an SQS queue. Karpenter polls the queue, drains the node, and launches a replacement before the instance is reclaimed.
resource "aws_cloudwatch_event_rule" "spot_interruption" { event_pattern = jsonencode({ source = ["aws.ec2"] "detail-type" = ["EC2 Spot Instance Interruption Warning"] }) } resource "aws_cloudwatch_event_target" "to_sqs" { rule = aws_cloudwatch_event_rule.spot_interruption.name arn = aws_sqs_queue.karpenter_interrupt.arn }
By combining these declarative settings, engineers achieve:
- Maximum utilization of lowâcost Spot capacity while preserving workload continuity.
- Continuous rightâsizing of nodes perâpod, eliminating overâprovisioned instances.
- Automated security and compliance updates through node expiry, supporting standards such as SOCâŻ2 and ISOâŻ27001 without manual ticketing.
Implement the above snippets in your Terraform or Helmâbased Karpenter deployment, and monitor the karpenter.sh/metrics endpoint to verify that consolidation events and Spot interruption drains are occurring as expected.
Step-by-Step Terraform Setup
Before provisioning Karpenter, understand the two distinct identities it requires: the controller pod that runs inside the EKS cluster, and the node role assumed by every EC2 instance Karpenter launches. The controller uses IAM Roles for Service Accounts (IRSA) to call AWS APIs, while the node role must be trusted by EC2 and include the standard workerânode policies.
- Controller role â created with an
assume_role_policythat references the clusterâs OIDC provider. The policy limits the principal to thekarpenterservice account in thekarpenternamespace. - Node role â a plain EC2 trust policy. After creation, attach the four AWSâmanaged policies required for EKS workers:
AmazonEKSWorkerNodePolicy,AmazonEKS_CNI_Policy,AmazonEC2ContainerRegistryPullOnly, andAmazonSSMManagedInstanceCore.
resource "aws_iam_role" "karpenter_controller" {
name = "KarpenterControllerRole-${var.cluster_name}"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:sub" = "system:serviceaccount:karpenter:karpenter"
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
}
}
}]
})
}
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"
}]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "node_policies" {
for_each = toset([
"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
])
role = aws_iam_role.karpenter_node.name
policy_arn = each.value
}
When the cluster uses authentication_mode = "API", replace the legacy aws-auth ConfigMap with an aws_eks_access_entry that grants the node role permission to join the cluster:
resource "aws_eks_access_entry" "karpenter_node" {
cluster_name = var.cluster_name
principal_arn = aws_iam_role.karpenter_node.arn
type = "EC2_LINUX"
}
Karpenter discovers subnets and security groups via tags rather than explicit IDs. Tag every private subnet and the node security group with the same key/value pair that matches the cluster name:
tags = {
"karpenter.sh/discovery" = var.cluster_name
}
With these resources in place, Terraform will provision the IAM roles, bind the node role to the cluster, and enable Karpenter to locate appropriate networking resources automatically. The next stepsâinstalling the Helm chart and defining NodePool and EC2NodeClass objectsâcan then reference the created roles without further manual configuration.
Advanced Configuration and Deployment
Karpenter improves spotâinstance safety by wiring AWSâŻEventBridge to an Amazon SQS queue that the controller polls. EventBridge captures the EC2 Spot Instance Interruption Warning (a twoâminute notice) and forwards the event to the queue via a CloudWatch Event rule and target. The controllerâs IAM policy must include sqs:ReceiveMessage, sqs:DeleteMessage, and sqs:GetQueueAttributes so it can drain the node before termination. The same pattern is repeated for rebalance recommendations, instanceâstate changes, and AWS Health events, all feeding the same queue.
To keep the controller policy in sync with upstream Karpenter releases, Terraformâs templatefile() function is used. The upstream JSON policy (published in the Karpenter cloudformation.yaml template) is stored locally, and environmentâspecific ARNs are injected at render time:
resource "aws_iam_role_policy" "karpenter_controller_policy" {
name = "KarpenterControllerPolicy-${var.cluster_name}"
role = aws_iam_role.karpenter_controller.id
policy = templatefile(
"${path.module}/policies/karpenter-controller-policy.json",
{
cluster_name = var.cluster_name
region = var.region
karpenter_node_role_arn = aws_iam_role.karpenter_node.arn
eks_cluster_arn = aws_eks_cluster.eks.arn
sqs_queue_arn = aws_sqs_queue.karpenter_interruption.arn
})
}
This approach guarantees that the JSON body remains identical to the upstream version; only the placeholders change per account, region, or cluster. Future upstream updates become a simple diff rather than a rewrite.
For Helmâbased installation, the recommended practice is to separate the Custom Resource Definitions (CRDs) from the controller deployment. Deploying them as distinct releases avoids accidental CRD overwrites during upgrades and aligns with Helmâs bestâpractice guidelines.
- StepâŻ1 â Install CRDs:
helm install karpenter-crds oci://public.ecr.aws/karpenter/karpenter-crds --version ${KARPENTER_VERSION} - StepâŻ2 â Install the controller:
helm install karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --set serviceAccount.create=false --set serviceAccount.name=karpenter --set controller.resources.requests.cpu=100m --set controller.resources.requests.memory=128Mi
Both releases should reference the same namespace (typically karpenter) and use the IAM service account created via IRSA. After Helm deployment, verify that the controller pod can read from the SQS queue and that the Node IAM role is attached to newly launched instances.
Looking for Custom Software or AI Solutions?
Appworks Technologies designs, builds, and scales production enterprise platforms, microservices, and AI agent workflows tailored to your business goals.
Editorial Policy & Research Methodology
Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.
