Terraform Modules in Action: Building a Production-Ready EKS Cluster from Scratch

As my Terraform journey is getting deeper, today’s learning was a major mindset shift.
Instead of writing one massive main.tf, I learned how Terraform Modules help you design infrastructure that is clean, reusable, scalable, and production-ready.
To apply this concept practically, I built a complete Amazon EKS cluster using Terraform, structured entirely around custom modules. This wasn’t just about spinning up Kubernetes — it was about engineering infrastructure the right way.
What Are Terraform Modules (In Simple Terms)?
A Terraform module is a logical container for related resources.
Think of it like a function in programming:
Inputs → variables
Logic → resources
Outputs → exposed values
Instead of repeating the same code everywhere, you define it once, reuse it many times, and manage it cleanly.
Visual Flow (How a module use the resources of another module )
aws_iam_role.cluster(iam_module
↓
output cluster_role_arn
↓
module.iam.cluster_role_arn
↓
passed from root module
↓
var.cluster_role_arn (eks module)
↓
aws_eks_cluster.role_arn
Why Modules Matter in Real Projects
Keeps infrastructure organized
Makes code reusable
Enables team collaboration
Reduces errors and duplication
Makes infra easier to scale and maintain
For complex systems like EKS, modules are not optional — they’re essential.
Project Overview: EKS Cluster Using Terraform Modules
I created a production-ready EKS cluster using AWS Provider 6.x and Terraform best practices.
High-Level Features
Custom VPC with public & private subnets (3 AZs)
Amazon EKS (Kubernetes v1.31)
Fargate for serverless pod execution (cost-optimized)
Core EKS add-ons
IAM Roles for Service Accounts (IRSA)
Clean modular Terraform structure
This project mimics real-world enterprise EKS setups, not toy examples.
Basic Folder Structure (MANDATORY)

Architecture Breakdown
Core Components
VPC: Networking foundation
IAM: Secure permissions for EKS & pods
EKS: Managed Kubernetes control plane
Fargate: Serverless execution for pods
ALB Controller: Ingress for external traffic
Module 1: VPC Module (modules/vpc/)
Purpose
Creates the networking foundation required by EKS.
Why This Module Is Critical
EKS requires properly tagged subnets
Kubernetes uses subnet tags for load balancers
NAT Gateway enables private workloads to pull images
Resources Created
1 VPC (
10.0.0.0/16)3 Public Subnets (3 AZs)
3 Private Subnets (3 AZs)
Internet Gateway
Single NAT Gateway (cost optimization)
Elastic IP
Route tables & associations
Smart Kubernetes Subnet Tagging
public_subnet_tags = { "kubernetes.io/role/elb" = "1"}
private_subnet_tags = { "kubernetes.io/role/internal-elb" = "1"}
This ensures Kubernetes automatically knows where to place load balancers.
Module 2: IAM Module (modules/iam/)
Purpose
Handles all IAM roles and permissions securely.
Why IAM Deserves Its Own Module
EKS control plane needs AWS permissions
Fargate requires execution roles
IRSA allows pods to assume IAM roles (no static keys!)
Resources Created
EKS Cluster IAM Role
EKS Fargate Execution Role
AWS Managed Policy Attachments:
AmazonEKSClusterPolicyAmazonEKSVPCResourceControllerAmazonEKSFargatePodExecutionRolePolicy
This module keeps security clean, auditable, and modular.
Module 3: EKS Module (modules/eks/)
Purpose
Creates the Kubernetes control plane and integrations.
Control Plane Components
EKS Cluster
KMS Key for etcd encryption
CloudWatch Log Group for cluster logs
Fargate
Fargate Profile for serverless pod execution
No EC2 worker node management
Pay only for running pods
Add-ons Installed
CoreDNS
kube-proxy
VPC CNI
IRSA Setup
OIDC Provider enabled
Allows pods to use AWS services securely
Example of Root Module(main.tf):
module "vpc" {
source = "./modules/vpc"
name_prefix = var.cluster_name
vpc_cidr = var.vpc_cidr
azs = slice(data.aws_availability_zones.available.names, 0, 3)
private_subnets = var.private_subnets
public_subnets = var.public_subnets
enable_nat_gateway = true
single_nat_gateway = true
# Required tags for EKS
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
}
tags = {
Environment = var.environment
Terraform = "true"
Project = "EKS-Day20"
}
}
# Custom IAM Module
module "iam" {
source = "./modules/iam"
cluster_name = var.cluster_name
tags = {
Environment = var.environment
Terraform = "true"
Project = "EKS-Day20"
}
}
# Custom EKS Module
module "eks" {
source = "./modules/eks"
cluster_name = var.cluster_name
kubernetes_version = var.kubernetes_version
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
cluster_role_arn = module.iam.cluster_role_arn
fargate_role_arn = module.iam.fargate_role_arn
endpoint_public_access = true
endpoint_private_access = true
public_access_cidrs = ["0.0.0.0/0"]
enable_irsa = true
tags = {
Environment = var.environment
Terraform = "true"
Project = "EKS-Day20"
}
depends_on = [module.iam]
}
Step-by-Step Deployment Process
Prerequisites
aws --version
terraform --version
kubectl version
helm version
Step 1: Initialize Terraform
terraform init
What happens:
AWS provider downloaded
Backend initialized
Modules prepared
Step 2: Validate Configuration
terraform validate
Expected:
Success! The configuration is valid.
Step 3: Plan the Deployment
terraform plan
Typical output:
Plan: 35 to add, 0 to change, 0 to destroy.
Always review:
CIDR blocks
Resource names
Estimated cost
Step 4: Apply Infrastructure
terraform apply
Type yes when prompted.
Configure kubectl Access
aws eks --region us-east-1 update-kubeconfig --name Terraform-eks
Verify:
kubectl cluster-info
kubectl get nodes
Setting Up AWS Load Balancer Controller (ALB)
This part taught me real-world IRSA implementation.
Step 1: Download IAM Policy
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json
Step 2: Create IAM Policy
aws iam create-policy \ --policy-name AWSLoadBalancerControllerIAMPolicy \ --policy-document file://iam_policy.json
Step 3: Create IAM Role with OIDC Trust
This is where IRSA magic happens.
Trust policy connects:
Kubernetes ServiceAccount
IAM Role
OIDC Provider
Create trust-policy.json:
Create trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller",
"oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:aud": "sts.amazonaws.com"
}
}
}
]
}
Create the role:
aws iam create-role --role-name AmazonEKSLoadBalancerControllerRole --assume-role-policy-document [file://trust-policy.json](file://trust-policy.json)
Step 4: Create ServiceAccount
kubectl create serviceaccount aws-load-balancer-controller -n kube-system
Annotate:
kubectl annotate serviceaccount aws-load-balancer-controller \ -n kube-system \ eks.amazonaws.com/role-arn=arn:aws:iam::<ACCOUNT_ID>:role/AmazonEKSLoadBalancerControllerRole
Step 5: Install ALB Controller Using Helm
helm repo add eks https://aws.github.io/eks-chartshelm repo updatehelm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system \ --set clusterName=<cluster-name> \ --set serviceAccount.create=false \ --set serviceAccount.name=aws-load-balancer-controller \ --set region=<region> \ --set vpcId=<vpc-id>
Verify:
kubectl get deployment -n kube-system aws-load-balancer-controller
Deploying a Sample Application (2048 Game 🎮)
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/examples/2048/2048_full.yaml
Check ingress:
kubectl get ingress
Copy the domain → open in browser → app is live!

📺 Video That Helped Me Understand this concept:
Full Source Code on GitHub
To keep the article focused on concepts and architecture, I’ve shared the complete Terraform codebase (including all modules, variables, and outputs) on GitHub:
🔗 GitHub Repository: DAY 20
👉 https://github.com/Rajesh180118/aws-terraform-mastery
Final Takeaways from Today
Terraform Modules are non-negotiable for real projects
EKS setup is 50% infra, 50% IAM & networking
IRSA is the right way to give AWS access to pods
Fargate simplifies ops and reduces costs
Modular Terraform = scalable DevOps mindset
This project made Terraform feel less like a tool and more like an engineering discipline.



