EKS with Terraform in Terraform, IaC & Cloud Automation
Managing Kubernetes clusters can feel like juggling fire when you're doing it manually through the AWS console. EKS with Terraform lets you define your entire cluster—control plane, node groups, networking, and access policies—as versioned code, giving you the reproducibility and safety that production workloads demand. By day 58 of your Terraform, IaC & Cloud Automation journey, you're ready to move beyond basic compute resources and tackle container orchestration at scale.
Why EKS Through Terraform Matters
We've all been in situations where a cluster was created by someone who left the company, documented nowhere, and configured differently from everything else. EKS with Terraform solves that. Your cluster becomes a tracked artifact in version control, auditable, reviewable, and reproducible. The Terraform AWS provider gives you first-class support for EKS resources, meaning you're not fighting API inconsistencies or workarounds.
For teams running microservices, multi-tenant SaaS, or any application that benefits from Kubernetes' orchestration, defining your cluster infrastructure through code is non-negotiable. You get drift detection, easy teardown for ephemeral environments, and the ability to spin up new clusters for testing without manual toil.
Use the Module — terraform-aws-modules/eks Handles Control Plane and Node Groups
The community-maintained terraform-aws-modules/eks module is your starting point. It encapsulates AWS best practices and saves you from writing dozens of resources manually. The module handles the EKS cluster, node group configuration, IAM roles, VPC integration, and RBAC—all of which have subtle interdependencies you don't want to debug at 2 AM.
Here's a practical example. Suppose we're building a cluster for a ride-hailing backend that needs to scale with demand:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = "ride-hailing-prod"
cluster_version = "1.27"
cluster_endpoint_private_access = true
cluster_endpoint_public_access = true
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.private[*].id
eks_managed_node_group_defaults = {
instance_types = ["t3.large"]
}
eks_managed_node_groups = {
general = {
name = "general-node-group"
desired_size = 3
min_size = 2
max_size = 10
instance_types = ["t3.large"]
tags = {
Environment = "prod"
Workload = "general"
}
}
}
cluster_security_group_additional_rules = {
ingress_nodes_ephemeral_ports_tcp = {
description = "Nodes on ephemeral ports"
protocol = "tcp"
from_port = 1025
to_port = 65535
type = "ingress"
source_node_security_group = true
}
}
tags = {
Environment = "prod"
Team = "platform"
}
}
output "cluster_endpoint" {
value = module.eks.cluster_endpoint
description = "EKS cluster endpoint"
}
output "cluster_security_group_id" {
value = module.eks.cluster_security_group_id
description = "Security group ID attached to the EKS cluster"
}
output "cluster_iam_role_arn" {
value = module.eks.cluster_iam_role_arn
description = "IAM role ARN of the EKS cluster"
}The module abstracts away IAM policy attachments, security group rules, and subnet tag requirements. You declare what you need; it figures out the plumbing. The eks_managed_node_groups block creates managed node groups—AWS handles patching and updates, so you're not managing EC2 instances yourself.
Notice we're not creating networking manually. The module expects you to provide VPC and subnet IDs, which you should provision separately (via another module or inline). This separation of concerns keeps your Terraform readable and testable.
Node Groups — Managed Node Groups or Karpenter for Autoscaling
Node groups are where your pods run. You have two primary options: AWS-managed node groups or Karpenter, a newer CNCF project for efficient autoscaling.
Managed Node Groups
Managed node groups are the simpler choice. AWS handles OS patching, security updates, and Auto Scaling Group management. You define desired, minimum, and maximum sizes, and AWS scales within those bounds based on Kubernetes resource requests.
In the example above, the general node group scales from 2 to 10 instances. AWS monitors cluster demand (via metrics from the cluster autoscaler running as a pod) and scales up or down. This works well for predictable workloads or when you need simple, low-maintenance scaling.
Here's adding a second node group optimized for GPU workloads (say, for a trading platform running ML inference):
eks_managed_node_groups = {
general = {
name = "general-node-group"
desired_size = 3
min_size = 2
max_size = 10
instance_types = ["t3.large"]
}
gpu_workloads = {
name = "gpu-node-group"
desired_size = 1
min_size = 0
max_size = 5
instance_types = ["g4dn.xlarge"]
taints = [{
key = "workload"
value = "gpu"
effect = "NO_SCHEDULE"
}]
labels = {
workload = "gpu"
}
tags = {
WorkloadType = "gpu"
}
}
}The taints and labels ensure GPU pods land on GPU nodes and prevent general workloads from consuming expensive GPU capacity.
Karpenter for Advanced Autoscaling
Karpenter is a CNCF project that provides more dynamic, cost-optimized autoscaling. Instead of pre-defining node groups, Karpenter launches the right instance type on-demand based on pod resource requirements. It consolidates underutilized nodes and can even swap between instance families to optimize cost.
Setting up Karpenter requires two layers: Terraform to create the IRSA (IAM Role for Service Account), and Helm to deploy the Karpenter controller. We'll cover the Terraform piece here:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "~> 19.0"
cluster_name = module.eks.cluster_name
irsa_oidc_provider = module.eks.oidc_provider
create_node_iam_role = true
tags = {
Environment = "prod"
}
}
output "karpenter_irsa_arn" {
value = module.karpenter.irsa_arn
}This creates the IAM role that Karpenter's controller uses to launch EC2 instances on your behalf. You then deploy the Karpenter Helm chart (not Terraform) and define provisioners—policies that specify which instance families and zones Karpenter can use. Karpenter consolidates nodes automatically, evicting low-priority pods if needed, which can reduce costs by 30–50% compared to managed node groups with fixed scaling policies.
Access — EKS Access Entries Instead of aws-auth ConfigMap
For years, EKS access was managed through the aws-auth ConfigMap, a Kubernetes object that defined who could connect to the cluster. This approach was error-prone and created a bootstrap problem: you needed initial access to edit the ConfigMap.
AWS now offers EKS Access Entries, a native resource you can manage via Terraform. It's cleaner, auditable, and integrates with AWS IAM without ConfigMap gymnastics.
resource "aws_eks_access_entry" "platform_team" {
cluster_name = module.eks.cluster_name
principal_arn = data.aws_iam_role.platform_team_role.arn
kubernetes_groups = ["platform-admins"]
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "platform_admin" {
cluster_name = module.eks.cluster_name
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
principal_arn = data.aws_iam_role.platform_team_role.arn
access_scope_type = "cluster"
}
data "aws_iam_role" "platform_team_role" {
name = "platform-team-role"
}
# Grant read-only access to a CI/CD service account
resource "aws_eks_access_entry" "ci_cd" {
cluster_name = module.eks.cluster_name
principal_arn = aws_iam_role.ci_cd.arn
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "ci_cd_read" {
cluster_name = module.eks.cluster_name
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy"
principal_arn = aws_iam_role.ci_cd.arn
access_scope_type = "cluster"
}Now your access control is version-controlled, auditable, and tied directly to AWS IAM roles. No more manual ConfigMap edits. Kubernetes groups are still assigned, but the source of truth is Terraform and AWS, not a Kubernetes ConfigMap. This is particularly valuable in regulated industries where audit trails matter.
Separate App Deployment — Use Helm or GitOps for Workloads, Not Terraform
A common mistake is trying to deploy every pod, service, and ingress through Terraform. Terraform is great for infrastructure, but Kubernetes workload management is a different concern. We deploy applications using Helm charts or GitOps tools like ArgoCD.
Your Terraform should stop at the cluster boundary. Once the cluster exists and your team has access, application teams deploy their services. Helm charts live in your application repositories, versioned independently of infrastructure.
# This is fine: Terraform installs the Helm provider and bootstraps the cluster
resource "helm_release" "ingress_nginx" {
name = "ingress-nginx"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
namespace = "ingress-nginx"
create_namespace = true
set {
name = "controller.service.type"
value = "LoadBalancer"
}
}
# This is NOT: Don't deploy every microservice via Terraform
# Instead, application teams manage their Helm releases or ArgoCD ApplicationSetsFor bootstrapping cluster add-ons (ingress controllers, monitoring, logging), Helm via Terraform is appropriate. But application workloads should flow through your CI/CD pipeline and GitOps controller, not Terraform apply. The cluster is infrastructure; the applications are cargo. Conflating them makes deployments slow, state management complex, and debugging nightmares.
Common Mistakes
- Mixing infrastructure and workloads: Deploying application services via
kubernetes_deploymentorhelm_releasein your infrastructure Terraform creates coupling and makes local development harder. Separate concerns: infrastructure as Terraform, workloads as Helm or GitOps. - Not using the module: Writing every EKS resource manually is reinventing the wheel. The terraform-aws-modules/eks module is battle-tested and handles edge cases you'll discover painfully otherwise.
- Hardcoding subnets and security groups: If your VPC is defined elsewhere (and it should be), reference it via data sources or outputs. Avoid copy-pasting subnet IDs into your cluster module. Use variables and separate networking Terraform.
- Ignoring RBAC and access control: Defaulting to cluster-admin for everyone or using outdated aws-auth ConfigMap management. Use EKS Access Entries and least-privilege access policies from the start.
- Scaling without a plan: Choosing between managed node groups and Karpenter without understanding your workload patterns. Managed node groups suit stable, predictable workloads; Karpenter shines for bursty, diverse workloads. Pick based on your needs, not defaults.
Key Takeaways
- Use
terraform-aws-modules/eksto abstract EKS complexity and avoid reinventing IAM, security groups, and networking configurations. - Choose managed node groups for simplicity or Karpenter for cost optimization and dynamic scaling—Terraform handles the setup, Kubernetes handles the runtime.
- Migrate from
aws-authConfigMaps to EKS Access Entries for audit-friendly, version-controlled access management. - Stop at the cluster boundary: Terraform creates infrastructure, Helm and GitOps deploy workloads. Separation keeps both fast and maintainable.
- Treat your cluster as immutable infrastructure—version every change in Terraform, review via pull requests, and use CI/CD to apply safely.






Comments
Loading comments…