This project provisions a fully production-grade Amazon EKS cluster on AWS using Terraform as the infrastructure as code tool. You will build the complete networking foundation (VPC, subnets, route tables, NAT gateways), the EKS control plane, managed node groups with autoscaling, IAM roles using IRSA (IAM Roles for Service Accounts), and a working NGINX Ingress Controller exposed via an AWS Load Balancer. This is the same foundational architecture used by teams at Zerodha, Swiggy, and Razorpay for running containerised workloads at scale. Region: ap-south-1 +---------------------------------------------------------+ | VPC | | +--------------+ +--------------+ | | | Public | | Private | | | | Subnet AZ-a | | Subnet AZ-a | | | | (NAT GW) | | (Node Group) | | | +--------------+ +--------------+ | | +--------------+ +--------------+ | | | Public | | Private | | | | Subnet AZ-b | | Subnet AZ-b | | | | (ALB) | | (Node Group) | | | +--------------+ +--------------+ | | | | +--------------------+ | | | EKS Control Plane | | | | (AWS Managed) | | | +--------------------+ | +---------------------------------------------------------+ | | Internet Gateway Private Node Groups | (t3.medium, 2–5 nodes) ALB Ingress
Manually creating an EKS cluster through the AWS console is not repeatable, not auditable, and breaks when someone makes a change through the console that is not tracked anywhere. Every environment — dev, staging, production — ends up slightly different, which causes "works in staging, broken in prod" situations. This project solves that entirely. One Terraform configuration, version-controlled in Git, produces identical EKS clusters in any environment. The cluster includes production security defaults — nodes in private subnets, IRSA instead of node-level IAM permissions, IMDSv2 enforced, and security group least-privilege rules.
### Step 1: Set Up Your Terraform Workspace Before writing any infrastructure code, set up the workspace correctly. This prevents state corruption and credential exposure. **Install required tools:** ```bash ## Install Terraform (use tfenv for version management) git clone https://github.com/tfutils/tfenv.git ~/.tfenv export PATH="$HOME/.tfenv/bin:$PATH" tfenv install 1.7.0 tfenv use 1.7.0 terraform --version ## Install AWS CLI v2 curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip unzip awscliv2.zip && sudo ./aws/install aws --version ## Install kubectl curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" chmod +x kubectl && sudo mv kubectl /usr/local/bin/ kubectl version --client ``` **Configure AWS credentials using a dedicated IAM role (never use root):** ```bash aws configure ## Enter: Access Key ID, Secret Access Key, Region (ap-south-1), Output (json) aws sts get-caller-identity # Verify — should return your account ID ``` **Create the S3 backend and DynamoDB lock table:** ```bash ## Create S3 bucket for state (replace YOUR_ACCOUNT_ID) aws s3api create-bucket \ --bucket eks-terraform-state-YOUR_ACCOUNT_ID \ --region ap-south-1 \ --create-bucket-configuration LocationConstraint=ap-south-1 ## Enable versioning on the state bucket aws s3api put-bucket-versioning \ --bucket eks-terraform-state-YOUR_ACCOUNT_ID \ --versioning-configuration Status=Enabled ## Create DynamoDB lock table aws dynamodb create-table \ --table-name terraform-eks-lock \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region ap-south-1 ``` > 📌 **Remember:** The S3 bucket name must be globally unique. Add your AWS account ID as a suffix to guarantee uniqueness. ### Step 2: Write the VPC Module Create the directory structure first: ```bash mkdir -p eks-terraform/{modules/vpc,modules/eks,modules/node-group,environments/production} cd eks-terraform ``` Create `modules/vpc/main.tf`: ```hcl terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_hostnames = true enable_dns_support = true tags = merge(var.tags, { Name = "${var.cluster_name}-vpc" }) } resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index) availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true tags = merge(var.tags, { Name = "${var.cluster_name}-public-${var.availability_zones[count.index]}" "kubernetes.io/role/elb" = "1" # Required for ALB Ingress Controller }) } resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + 4) availability_zone = var.availability_zones[count.index] tags = merge(var.tags, { Name = "${var.cluster_name}-private-${var.availability_zones[count.index]}" "kubernetes.io/role/internal-elb" = "1" # Required for internal ALB }) } resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = merge(var.tags, { Name = "${var.cluster_name}-igw" }) } resource "aws_eip" "nat" { count = length(var.availability_zones) domain = "vpc" tags = merge(var.tags, { Name = "${var.cluster_name}-nat-eip-${count.index}" }) } resource "aws_nat_gateway" "main" { count = length(var.availability_zones) allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id tags = merge(var.tags, { Name = "${var.cluster_name}-nat-${count.index}" }) depends_on = [aws_internet_gateway.main] } ``` > ⚠️ **Security:** EKS worker nodes must go in private subnets. Never place nodes in public subnets — this exposes the kubelet API and node metadata endpoint to the internet. ### Step 3: Write the EKS Cluster Module Create `modules/eks/main.tf`: ```hcl resource "aws_eks_cluster" "main" { name = var.cluster_name version = var.kubernetes_version role_arn = aws_iam_role.cluster.arn vpc_config { subnet_ids = var.private_subnet_ids endpoint_private_access = true endpoint_public_access = true # Set to false for fully private clusters public_access_cidrs = var.allowed_cidr_blocks } # Enable envelope encryption for Kubernetes secrets encryption_config { provider { key_arn = aws_kms_key.eks.arn } resources = ["secrets"] } enabled_cluster_log_types = [ "api", "audit", "authenticator", "controllerManager", "scheduler" ] depends_on = [ aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy, aws_cloudwatch_log_group.eks ] tags = var.tags } ## KMS key for secret encryption resource "aws_kms_key" "eks" { description = "EKS Secret Encryption Key - ${var.cluster_name}" deletion_window_in_days = 7 enable_key_rotation = true tags = var.tags } ## IAM role for the EKS control plane resource "aws_iam_role" "cluster" { name = "${var.cluster_name}-cluster-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "eks.amazonaws.com" } }] }) tags = var.tags } resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" role = aws_iam_role.cluster.name } ## CloudWatch log group for control plane logs resource "aws_cloudwatch_log_group" "eks" { name = "/aws/eks/${var.cluster_name}/cluster" retention_in_days = 30 tags = var.tags } ``` ### Step 4: Configure Managed Node Groups with IRSA Create `modules/node-group/main.tf`: ```hcl resource "aws_eks_node_group" "main" { cluster_name = var.cluster_name node_group_name = "${var.cluster_name}-${var.node_group_name}" node_role_arn = aws_iam_role.node.arn subnet_ids = var.private_subnet_ids instance_types = var.instance_types capacity_type = var.capacity_type # ON_DEMAND or SPOT scaling_config { desired_size = var.desired_size max_size = var.max_size min_size = var.min_size } update_config { max_unavailable = 1 } # Launch template for IMDSv2 enforcement launch_template { id = aws_launch_template.node.id version = aws_launch_template.node.latest_version } labels = var.node_labels lifecycle { ignore_changes = [scaling_config[0].desired_size] } depends_on = [ aws_iam_role_policy_attachment.node_AmazonEKSWorkerNodePolicy, aws_iam_role_policy_attachment.node_AmazonEKS_CNI_Policy, aws_iam_role_policy_attachment.node_AmazonEC2ContainerRegistryReadOnly ] tags = var.tags } ## Launch template enforcing IMDSv2 (prevents SSRF attacks on metadata endpoint) resource "aws_launch_template" "node" { name_prefix = "${var.cluster_name}-node-" metadata_options { http_endpoint = "enabled" http_tokens = "required" # Forces IMDSv2 http_put_response_hop_limit = 1 } tag_specifications { resource_type = "instance" tags = merge(var.tags, { Name = "${var.cluster_name}-node" }) } } ``` > 💡 **Tip:** Set `capacity_type = "SPOT"` for non-production node groups to reduce costs by up to 70%. Always use `ON_DEMAND` for production stateful workloads. ### Step 5: Wire Everything Together in the Production Environment Create `environments/production/main.tf`: ```hcl terraform { required_version = ">= 1.7.0" backend "s3" { bucket = "eks-terraform-state-YOUR_ACCOUNT_ID" key = "production/eks/terraform.tfstate" region = "ap-south-1" encrypt = true dynamodb_table = "terraform-eks-lock" } } provider "aws" { region = var.aws_region } module "vpc" { source = "../../modules/vpc" cluster_name = var.cluster_name vpc_cidr = "10.0.0.0/16" availability_zones = ["ap-south-1a", "ap-south-1b"] tags = local.common_tags } module "eks" { source = "../../modules/eks" cluster_name = var.cluster_name kubernetes_version = "1.29" private_subnet_ids = module.vpc.private_subnet_ids allowed_cidr_blocks = ["YOUR_OFFICE_IP/32"] # Restrict public API access tags = local.common_tags } module "node_group" { source = "../../modules/node-group" cluster_name = module.eks.cluster_name node_group_name = "general" private_subnet_ids = module.vpc.private_subnet_ids instance_types = ["t3.medium"] capacity_type = "ON_DEMAND" desired_size = 2 min_size = 2 max_size = 5 tags = local.common_tags } locals { common_tags = { Environment = "production" ManagedBy = "terraform" Project = var.cluster_name } } ``` ### Step 6: Deploy and Configure kubectl Access ```bash ## Initialise Terraform cd environments/production terraform init ## Preview what will be created (always review before applying) terraform plan -out=tfplan ## Apply — this takes approximately 12-15 minutes terraform apply tfplan ## Configure kubectl to connect to the new cluster aws eks update-kubeconfig \ --region ap-south-1 \ --name YOUR_CLUSTER_NAME ## Verify nodes are Ready kubectl get nodes -o wide ``` ### Step 7: Install the AWS Load Balancer Controller The AWS Load Balancer Controller provisions ALBs automatically when you create a Kubernetes Ingress resource. ```bash ## Install Helm curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash ## Add the EKS chart repository helm repo add eks https://aws.github.io/eks-charts helm repo update ## Create the IAM policy for the controller aws iam create-policy \ --policy-name AWSLoadBalancerControllerIAMPolicy \ --policy-document file://iam-policy.json ## Create the service account with IRSA eksctl create iamserviceaccount \ --cluster=YOUR_CLUSTER_NAME \ --namespace=kube-system \ --name=aws-load-balancer-controller \ --role-name AmazonEKSLoadBalancerControllerRole \ --attach-policy-arn=arn:aws:iam::YOUR_ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \ --approve ## Install the controller helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ -n kube-system \ --set clusterName=YOUR_CLUSTER_NAME \ --set serviceAccount.create=false \ --set serviceAccount.name=aws-load-balancer-controller ## Verify kubectl get deployment -n kube-system aws-load-balancer-controller ``` > 🔴 **Common Mistake:** Forgetting to add the `kubernetes.io/role/elb: "1"` tag on public subnets. The ALB controller uses this tag to discover which subnets to place load balancers in. If the tag is missing, Ingress resources will stay in a pending state with no ALB provisioned.
Run these checks to confirm your cluster is healthy and production-ready: ```bash ## 1. Verify all nodes are Ready kubectl get nodes ## Expected: All nodes show STATUS = Ready ## 2. Check system pods are running kubectl get pods -n kube-system ## Expected: coredns, aws-node, kube-proxy, aws-load-balancer-controller all Running ## 3. Deploy a test application to verify end-to-end kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: nginx-test spec: replicas: 2 selector: matchLabels: app: nginx-test template: metadata: labels: app: nginx-test spec: containers: * name: nginx image: nginx:1.25-alpine ports: * containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-test spec: selector: app: nginx-test ports: * port: 80 targetPort: 80 type: LoadBalancer EOF ## 4. Wait for the LoadBalancer to provision (1-2 minutes) kubectl get svc nginx-test --watch ## 5. Test the endpoint curl http://$(kubectl get svc nginx-test -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') ## Expected: nginx welcome page HTML ## 6. Verify Terraform state is clean terraform plan ## Expected: No changes. Infrastructure is up-to-date. ## 7. Clean up test resources kubectl delete deployment nginx-test kubectl delete svc nginx-test ```
This project provisions a fully production-grade Amazon EKS cluster on AWS using Terraform as the infrastructure as code...
Manually creating an EKS cluster through the AWS console is not repeatable, not auditable, and breaks when someone makes...
Step 1: Set Up Your Terraform Workspace Before writing any infrastructure code, set up the workspace correctly. This pre...
Run these checks to confirm your cluster is healthy and production-ready:...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.