Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 058 / 100

EKS with Terraform in Terraform, IaC & Cloud Automation

Kubernetes clusters as code.

Expona Admin9 min read

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:

hcl
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):

hcl
  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:

hcl
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.

hcl
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.

hcl
# 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 ApplicationSets

For 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_deployment or helm_release in 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/eks to 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-auth ConfigMaps 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles