Articles

Enabling Karpenter on EKS with Terraform: What It Is, Why It's Worth It, and How to Set It Up

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.

Written by:
APin

Senior Technology Analyst • Verified Expert

More from this author
Enabling Karpenter on EKS with Terraform: What It Is, Why It's Worth It, and How to Set It Up

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:

  1. Reads the pod’s resources.requests (CPU, memory), nodeSelector constraints (architecture, zone, GPU, etc.), and any custom karpenter.sh labels.
  2. Queries the full catalog of EC2 instance types available in the target region.
  3. Selects the cheapest instance that satisfies all constraints, preferring Spot capacity when the capacity-type label allows it.
  4. Calls ec2:CreateFleet to launch the instance immediately, bypassing the ASG provisioning cycle.
  5. 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 Pending and 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 NodePools and EC2NodeClasses. 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 expireAfter policies, 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 NodePool spec. 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_policy that references the cluster’s OIDC provider. The policy limits the principal to the karpenter service account in the karpenter namespace.
  • Node role – a plain EC2 trust policy. After creation, attach the four AWS‑managed policies required for EKS workers: AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryPullOnly, and AmazonSSMManagedInstanceCore.
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.

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.

Have an Idea?

Let's Build Something Amazing Together.