Deploy Karpenter and Metrics Server on Amazon EKS using Terraform and Helm

Amazon EKS manages the control plane, but managing the data plane, the EC2 instances on which pods run, is the customer’s responsibility. To provision data plane capacity, you create managed node groups backed by Auto Scaling Groups (ASGs), with a launch template that locks in instance types, capacity type (on-demand or spot), and scaling limits.

However, EKS does not automatically scale nodes in response to pod scheduling pressure. If all existing nodes are full and new pods can’t be scheduled, those pods remain in a Pending state until capacity is added. Traditionally, teams solve this by deploying the Kubernetes Cluster Autoscaler, an open-source component that watches for unschedulable pods and increases the ASG’s desired count. This works, but it has limitations: ASGs scale in fixed increments, offer no cost-aware instance selection, and react slowly because they operate at the node group level rather than responding directly to individual pod requirements.

Karpenter takes a different approach. It watches for unscheduled pods and then provisions the optimal EC2 instance type, size, availability zone, and purchase option (on-demand or spot) for each workload in real time, unconstrained by launch templates. When demand drops, Karpenter consolidates workloads onto fewer, cheaper nodes and terminates the rest. This results in faster scaling (under two minutes from pending pod to running in our test) and lower compute costs through right-sizing and spot usage.

While Karpenter handles node-level scaling, something still needs to decide when pods themselves should scale. That’s where Metrics Server comes in. It collects real-time CPU and memory usage from every kubelet in the cluster and exposes those metrics through the Kubernetes Metrics API. Without it, the Horizontal Pod Autoscaler (HPA) has no data to act on and cannot add or remove pod replicas in response to load. Metrics Server is the foundation that enables pod autoscaling, and pod autoscaling generates the unschedulable pods that trigger Karpenter. The two components form a feedback loop: Metrics Server enables HPA to scale pods, and Karpenter ensures there’s always capacity to run them.

This article demonstrates how to deploy Karpenter and the Metrics Server to an EKS cluster using Terraform and Helm. You’ll learn how to configure Karpenter’s IAM permissions, set up interruption handling for spot instances, define a NodePool and EC2NodeClass, and validate the setup by triggering and observing a complete scaling event.

Solution Overview

This solution extends the multi-configuration Terraform architecture from the previous article. The infrastructure configuration adds Karpenter’s IAM role, policy, Pod Identity association, and an SQS queue with EventBridge rules for interruption handling. The platform configuration adds two Helm releases (Karpenter and Metrics Server) and applies NodePool and EC2NodeClass manifests that tell Karpenter what it’s allowed to provision and how to launch it.
Karpenter and Metrics Server architecture diagram
I organized the use case into three stages:

1. Infrastructure configuration: Create the IAM role and policy for Karpenter, set up the SQS interruption queue with EventBridge rules, add discovery tags to subnets and security groups, and store the outputs in the encrypted SSM parameter.
2. Platform configuration: Install Metrics Server and Karpenter via Helm, then apply the NodePool and EC2NodeClass CRD manifests.
3. Scaling validation: Deploy an “inflated workload” to trigger Karpenter, observe node provisioning, then scale down and watch consolidation.

You can find the complete implementation in my GitHub repository: kunduso-org/aws-eks-terraform (branch: deploy-karpenter). The code includes Terraform configurations, GitHub Actions CI/CD, and security scanning with Checkov.

Prerequisites

This use case builds on the previous two articles in this series: Provision a Secure Amazon EKS Cluster using Terraform and GitHub Actions and Deploy AWS Load Balancer Controller with Multi-Configuration Terraform and Helm. Please go through those before starting here.

Implementation

Step 1: Update subnet and security group tags for Karpenter
Karpenter schedules the Amazon EC2 instances in the subnet with the tag key karpenter.sh/discovery. Following security best practices, I chose to create these tags in the private subnets so that the EC2 instances were hosted there. Before provisioning these instances, Karpenter also requires the security group ids to attach to them. That is also queried using the same tag key karpenter.sh/discovery in a security group.

Since both resources were already provisioned (in the first article), I only added the additional tag metadata to the existing resources.

The image below shows the tag attached to the private subnet.

And here’s the image of adding the tag property to the node’s security group.

Step 2: Create the IAM role for the Karpenter pod
Karpenter runs as a deployment in the karpenter namespace inside the EKS cluster. That translates to two pods (active and standby). These pods need AWS permissions to launch and terminate EC2 instances, granted via Pod Identity. That is managed via aws_iam_role, aws_iam_role_policy, and aws_eks_pod_identity_association AWS provider resources.

The aws_iam_role resource creates the IAM role Karpenter’s pods assume.

The aws_iam_role_policy resource defines permissions for the Karpenter role.

This resource associates the Karpenter IAM role with the detailed policy document as specified in karpenter-iam-policy.tf. The policy grants permissions to launch and terminate EC2 instances, manage launch templates and instance profiles, discover available instance types and AMIs, and poll the interruption queue.

Finally, this IAM role (with the attached custom policy) is associated with the aws_eks_pod_identity_association resources, where the namespace and service_account properties are specified.

Step 3: Set up the SQS interruption queue with EventBridge rules
One way Karpenter reduces compute costs is by utilizing spot EC2 instances. Spot instances let you use spare AWS compute capacity at up to 90% cheaper than on-demand, but AWS can reclaim them with as little as two minutes’ notice. Without advance warning, pods on that instance would terminate abruptly, potentially interrupting workloads.

Beyond spot reclamation, EC2 instances can also be affected by scheduled maintenance events, unexpected state changes (an instance stopped or terminated externally), and capacity reservation interruptions. In all these cases, Karpenter needs early notice so it can cordon off the affected node (EC2 instance), drain its pods, migrate them to healthy nodes, and launch a replacement before the instance disappears.

The interruption queue provides that early warning system. EventBridge rules capture these lifecycle events from AWS and forward them to an SQS queue. Karpenter polls this queue and, when it receives a signal, initiates a graceful migration: cordon, drain, and replace.

3.1: Create the SQS queue and policy
Karpenter polls the AWS SQS queue for lifecycle event information. In this step, I created the SQS queue for Karpenter.

After creating the queue, I attached a permission policy to SQS for the events.amazonaws.com and sqs.amazonaws.com service principals to send messages to this queue.

3.2: Create event rules and targets for the rules
Karpenter watches for five separate kinds of events. These are:

  • AWS Health Event
  • EC2 Spot Instance Interruption Warning
  • EC2 Instance Rebalance Recommendation
  • EC2 Instance State-change Notification
  • EC2 Capacity Reservation Instance Interruption Warning

And upon receiving them, using the resource aws_cloudwatch_event_target, the SQS queue is populated with the event, ready for Karpenter to access. Below is an image of the aws_cloudwatch_event_rule resource for spot EC2 instance interruption and the aws_cloudwatch_event_trigger to the Karpenter interruption SQS queue.

Step 4: Install Karpenter and Metrics Server using the Helm provider
The Terraform code for Karpenter and Metrics Server is in the platform configuration, the same separate state file introduced in the previous article. The platform configuration uses the same encrypted SSM parameter handoff and Helm provider setup covered there, so I won’t repeat that here.

4.1 Install Metrics Server
Metrics Server is a Helm release with no custom values. The chart comes from the Kubernetes SIGs repository, version 3.13.0, deployed to the kube-system namespace. Once running, it exposes CPU and memory metrics via the Kubernetes Metrics API (metrics.k8s.io), enabling kubectl top commands and providing the data the Horizontal Pod Autoscaler needs to make scaling decisions.

4.2 Install Karpenter
Karpenter’s Helm chart is distributed as an OCI artifact from public.ecr.aws/karpenter rather than a traditional Helm repository. I installed version 1.12.0 into its own karpenter namespace with create_namespace = true.

The chart values pass two pieces of information from the infrastructure stack: the cluster name (so Karpenter knows which cluster to manage) and the interruption queue name (so it can poll for EC2 lifecycle events from Step 3). The service account name defaults to karpenter, corresponding to the Pod Identity association from Step 2. This completes the same authentication chain covered in the previous article: EKS sees the pod’s service account, finds the matching Pod Identity association, and injects the IAM role credentials.

Step 5: Create a sample deployment to validate scaling
To validate that Karpenter provisions nodes in response to scheduling pressure, I created a sample deployment in the scaling-check/ folder. The deployment runs 10 replicas of a minimal pause container, each requesting 1 CPU. This exceeds the capacity of the existing managed node group (2x t3.medium), forcing pods into a Pending state and prompting Karpenter to launch additional nodes.

Deployment

Since the three core workflows (infrastructure, platform, application) were covered in the previous article, I’ll focus on what’s new. The infrastructure workflow (terraform.yml) now deploys the Karpenter IAM role, interruption queue, and discovery tags alongside the existing resources. The platform workflow (deploy-platform.yml) installs Metrics Server and Karpenter via Helm in addition to the Load Balancer Controller.

This use case adds a fourth workflow (scaling-check.yml) to validate scaling. It runs manually via workflow_dispatch and has two stages separated by an approval gate:

1. Deploy inflate — prints the current node state, applies the inflate deployment, waits for Karpenter to provision nodes, then prints the updated nodes, pods, and NodeClaims.
2. Cleanup inflate — requires manual approval in the cleanup-approval environment, then deletes the inflate deployment and waits for Karpenter to consolidate, printing the final node state.

The approval gate gives you time to observe the scaled-out state before triggering consolidation.

Validation

After triggering the scaling-check workflow, I validated the full scaling lifecycle through the workflow logs and the AWS console.

The “before” step shows the cluster with two managed node group instances, the baseline state before any “inflate workload”.

After applying the inflate deployment and waiting 120 seconds, Karpenter provisioned a c5a.4xlarge instance to accommodate the 10 pods requesting 1 CPU each. The workflow logs show the new node in Ready state, all inflate pods running, and the NodeClaim tracking the provisioned instance.

The AWS console confirms the same: three nodes total, with the Karpenter-provisioned instance showing as “Self-managed” and managed by the default NodePool, distinct from the two managed node group instances.

After the cleanup stage deletes the “inflate workload” deployment, Karpenter detects the underutilized node and consolidates, terminating the extra capacity. The final node state returns to the original two managed nodes.

Conclusion

In this article, I walked through deploying Karpenter and Metrics Server on Amazon EKS using Terraform and Helm. The implementation covered the full setup: discovery tags on subnets and security groups, an IAM role with a least-privilege policy and Pod Identity association, an SQS interruption queue with EventBridge rules for graceful instance lifecycle handling, and the Helm releases for both Metrics Server and Karpenter. To validate, I deployed a workload that exceeded the existing node capacity, triggering Karpenter to provision a right-sized EC2 instance in real time, and then watched it consolidate back to baseline after the workload was removed.

In the next article, I’ll build on this by adding EKS access entries to control who can authenticate and what they can do within the cluster.

If you have any questions or suggestions, feel free to comment or get in touch.

Leave a Reply