100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Terraform & Infrastructure as Code
80 minintermediate

Capstone — Provision the CricketPulse Cluster

This capstone provisions the complete CricketPulse production infrastructure from a blank AWS account — everything needed to run a live cricket scores and statistics platform that handles IPL Final-scale traffic. The infrastructure includes: a custom VPC with public and private subnets across two availability zones; an EKS cluster (or ECS as an alternative) for containerised application workloads; an RDS PostgreSQL instance for persistent match data; an ElastiCache Redis cluster for live score caching; an S3 bucket for media assets; all networking (NAT gateways, security groups, route tables); IAM roles for the application; and secrets managed via AWS Secrets Manager. The entire infrastructure is built as a set of reusable Terraform modules in a well-structured repository, deployed through a CI/CD pipeline, with separate dev and production configurations. This project demonstrates mastery of every Terraform skill from this course working together in a realistic, production-grade scenario.

Analogy🏏Cricket
🏏 Think of it like cricket: This capstone is the full construction of the CricketPulse headquarters — building the stadium from foundation to first match in one continuous project. Every lesson in this course has been a specialist contractor: Module 1 taught the blueprint methodology (IaC principles), Module 2 taught the project management system (state and lifecycle), Module 3 taught the materials specification system (variables and expressions), Module 4 built the reusable construction components (modules), Module 5 set up the international supply chains and workforce management (multi-cloud and Ansible), and Module 6 covered the maintenance and operations systems (drift detection, security, CI/CD). The capstone is the opening day: all contractors work from the same blueprint, the project management system tracks every resource, and the stadium opens to 50,000 fans (the CricketPulse API goes live) having been built entirely by code — reproducible, auditable, and ready for the IPL Final.

Learning Objectives

  • Design and implement a multi-module Terraform repository structure with shared modules (vpc, eks-cluster, rds, elasticache) and environment-specific root modules (environments/dev, environments/production).
  • Provision a production-grade VPC with public subnets (load balancers), private subnets (application and database tiers), NAT gateways for private subnet internet access, and VPC endpoints for S3 and Secrets Manager.
  • Deploy an EKS cluster using the `terraform-aws-modules/eks/aws` community module with managed node groups, appropriate IAM roles, and OIDC provider for service account authentication.
  • Manage database credentials and API secrets using AWS Secrets Manager with Terraform data sources, ensuring no secrets appear in configuration files or CI/CD logs.
  • Implement the directory-per-environment pattern with separate state files, backend configurations, and variable values for dev (minimal resources, t3.micro instances) and production (HA configuration, production instance sizes).
  • Build and validate a GitHub Actions CI/CD pipeline that runs terraform plan on pull requests (with OIDC authentication and PR comment output) and terraform apply on merge (with manual approval and saved plan file).

Technical Requirements

  • Repository structure: `modules/` (vpc, eks-cluster, rds, elasticache, s3-media), `environments/dev/` and `environments/production/` each with `main.tf`, `variables.tf`, `outputs.tf`, `backend.tf`, and `terraform.tfvars.example`.
  • VPC module: creates VPC, 2 public subnets (AZs: ap-south-1a, ap-south-1b), 2 private subnets (app tier), 2 database subnets (db tier), internet gateway, NAT gateway (1 in dev, 2 in production for HA), and appropriate route tables.
  • EKS module: provisions EKS control plane, managed node group (t3.small × 1 in dev; t3.large × 3 in production with autoscaling 3–10), IAM OIDC provider, cluster IAM role, node IAM role with required managed policies.
  • RDS module: PostgreSQL 15, db.t3.micro (dev) / db.r6g.large Multi-AZ (production), subnet group in db subnets, security group allowing access only from app-tier security group, automated backups (7-day retention in production).
  • ElastiCache module: Redis 7, cache.t3.micro × 1 (dev) / cache.r6g.large × 2 in cluster mode (production), subnet group in private subnets, security group allowing access only from app-tier security group.
  • Secrets: DB password and API secret stored in AWS Secrets Manager, retrieved via data sources — zero plaintext secrets in any Terraform file or CI/CD log.
  • State: separate S3 + DynamoDB backend per environment, both buckets with KMS encryption, versioning, and public access blocked.
  • CI/CD: GitHub Actions plan job (PR, OIDC plan role, fmt/validate/plan, PR comment) and apply job (main merge, OIDC apply role, saved plan, production environment approval gate).

Architecture & Design

The CricketPulse cluster uses a three-tier VPC architecture. The public tier (public subnets) hosts the Application Load Balancer that receives external HTTPS traffic. The application tier (private subnets) hosts the EKS worker nodes running CricketPulse API pods. The data tier (database subnets — a special private subnet with no route to the NAT gateway) hosts the RDS PostgreSQL instance and ElastiCache Redis cluster. Security groups enforce tier-to-tier access: the ALB security group allows HTTPS from 0.0.0.0/0; the EKS node security group allows traffic only from the ALB security group; the RDS security group allows PostgreSQL only from the EKS node security group; the Redis security group allows port 6379 only from the EKS node security group. No tier can be accessed except through its designated entry point.

Analogy🏏Cricket
🏏 Think of it like cricket: The three-tier VPC architecture is like the CricketPulse stadium's security zones. The public stands (public subnets with ALB) are accessible to all ticket holders (internet traffic) — this is the official entry point. The players' pavilion (private subnets with EKS) is accessible only to players and official staff who enter through the official gate (ALB security group) — the general public cannot walk directly onto the pitch. The secure equipment vault (database subnets with RDS and Redis) is accessible only to the equipment manager with the specific vault key (EKS node security group) — even pavilion staff can't access the vault without the specific authorisation. Each security group is like the access control system on each zone's door — only the right badge opens the right door.
hcl
# Repository structure and VPC module

# Directory structure
cricketpulse-infra/
  modules/
    vpc/
      main.tf      # VPC, subnets, IGW, NAT, route tables
      variables.tf
      outputs.tf
    eks-cluster/
      main.tf
      variables.tf
      outputs.tf
    rds/
      main.tf
      variables.tf
      outputs.tf
    elasticache/
      main.tf
      variables.tf
      outputs.tf
  environments/
    dev/
      main.tf
      backend.tf
      terraform.tfvars.example  # Committed placeholder
    production/
      main.tf
      backend.tf
      terraform.tfvars.example
  .github/workflows/
    terraform.yml

# modules/vpc/main.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = merge(var.common_tags, { Name = "${var.name}-vpc" })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = merge(var.common_tags, { Name = "${var.name}-igw" })
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  tags = merge(var.common_tags, {
    Name = "${var.name}-public-${count.index + 1}"
    "kubernetes.io/role/elb" = "1"  # Required for EKS ALB controller
  })
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  tags = merge(var.common_tags, {
    Name = "${var.name}-private-${count.index + 1}"
    "kubernetes.io/role/internal-elb" = "1"
  })
}

resource "aws_subnet" "database" {
  count             = length(var.db_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.db_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  tags = merge(var.common_tags, { Name = "${var.name}-db-${count.index + 1}" })
}

resource "aws_eip" "nat" {
  count  = var.nat_gateway_count
  domain = "vpc"
  tags   = merge(var.common_tags, { Name = "${var.name}-nat-${count.index + 1}" })
}

resource "aws_nat_gateway" "main" {
  count         = var.nat_gateway_count
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags          = merge(var.common_tags, { Name = "${var.name}-nat-${count.index + 1}" })
}

# modules/vpc/outputs.tf
output "vpc_id"             { value = aws_vpc.main.id }
output "public_subnet_ids"  { value = aws_subnet.public[*].id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }
output "database_subnet_ids"{ value = aws_subnet.database[*].id }

Phase 1 — VPC and Networking

Implement the VPC module and the environment root modules. Deploy the dev environment VPC (single NAT gateway, smaller CIDR blocks) and verify all subnets, route tables, and the NAT gateway are created and correctly associated.

Analogy🏏Cricket
🏏 Think of it like cricket: building the VPC and networking first is like laying and marking the ground before anyone can play — the outfield, the boundary rope, the pitch, and the access gates must exist before a single fielder takes position. Just as you can't set a field on a ground that hasn't been laid out, no compute or database can live without a VPC, subnets, route tables, and a NAT gateway. Deploying the dev environment with a single NAT gateway and smaller CIDR blocks is like preparing a compact practice ground rather than a full stadium — enough to play and rehearse, at lower cost. Verifying every subnet and route table is correctly associated is like checking each boundary is properly roped and every gate leads where it should before the match. The payoff: like a correctly-marked ground that makes all subsequent play possible, a correctly-built network foundation lets every later tier — data and compute — take the field safely.
hcl
# environments/dev/main.tf — Phase 1: VPC only
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" { region = var.aws_region }

locals {
  name = "cricketpulse-${var.environment}"
  common_tags = {
    project     = "cricketpulse"
    environment = var.environment
    managed_by  = "terraform"
  }
}

module "vpc" {
  source = "../../modules/vpc"
  name                 = local.name
  vpc_cidr             = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"]
  db_subnet_cidrs      = ["10.0.20.0/24", "10.0.21.0/24"]
  availability_zones   = ["${var.aws_region}a", "${var.aws_region}b"]
  nat_gateway_count    = 1    # Dev: single NAT gateway (cost saving)
  common_tags          = local.common_tags
}

# Deploy and verify
# cd environments/dev && terraform init && terraform apply
# After apply:
output "vpc_id"              { value = module.vpc.vpc_id }
output "public_subnet_ids"   { value = module.vpc.public_subnet_ids }
output "private_subnet_ids"  { value = module.vpc.private_subnet_ids }

Phase 2 — Data Tier and Secrets

Add the RDS PostgreSQL and ElastiCache Redis modules with secrets managed via AWS Secrets Manager. The database password must never appear in any Terraform file. Deploy to dev and validate connectivity from within the VPC.

Analogy🏏Cricket
🏏 Think of it like cricket: adding the RDS PostgreSQL and ElastiCache Redis tier with secrets in AWS Secrets Manager is like installing the official scorebook and the team's confidential records in a locked vault, never scribbled on an open sheet. Just as the database is where CricketPulse's authoritative match data lives — the permanent record — RDS is the durable store and Redis the fast-access memory for recent play, like a scoreboard operator's quick tally versus the master scorebook. Crucially, the database password must never appear in any Terraform file, just as the vault combination is never written on the dressing-room wall — it's fetched from Secrets Manager at deploy time, like a manager retrieving the sealed code only when needed. Validating connectivity from within the VPC is like confirming only accredited staff inside the ground can reach the records. The payoff: your data tier stores everything reliably while its keys stay sealed, exactly as a team guards both its scorebook and the vault it lives in.
hcl
# modules/rds/main.tf
resource "aws_db_subnet_group" "main" {
  name       = "${var.name}-db-subnet-group"
  subnet_ids = var.database_subnet_ids
  tags       = var.common_tags
}

resource "aws_security_group" "rds" {
  name   = "${var.name}-rds"
  vpc_id = var.vpc_id
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [var.app_security_group_id]
  }
  tags = var.common_tags
}

resource "aws_db_instance" "main" {
  identifier        = var.name
  engine            = "postgres"
  engine_version    = "15.4"
  instance_class    = var.instance_class
  allocated_storage = var.storage_gb
  db_name           = "cricketpulse"
  username          = "cpadmin"
  password          = var.db_password    # Passed in from Secrets Manager data source
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  skip_final_snapshot    = var.skip_final_snapshot
  backup_retention_period = var.backup_retention_days
  tags = var.common_tags
}

output "endpoint" { value = aws_db_instance.main.endpoint }
output "db_name"  { value = aws_db_instance.main.db_name }

# environments/dev/secrets.tf — retrieve secrets from Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "cricketpulse/${var.environment}/db_password"
}

# Add to environments/dev/main.tf:
module "rds" {
  source = "../../modules/rds"
  name                    = "${local.name}-db"
  vpc_id                  = module.vpc.vpc_id
  database_subnet_ids     = module.vpc.database_subnet_ids
  app_security_group_id   = module.eks.node_security_group_id
  db_password             = sensitive(data.aws_secretsmanager_secret_version.db_password.secret_string)
  instance_class          = var.db_instance_class   # t3.micro for dev
  storage_gb              = 20
  skip_final_snapshot     = true   # Dev: no final snapshot
  backup_retention_days   = 1
  common_tags             = local.common_tags
}

Phase 3 — EKS Cluster and Final Assembly

Add the EKS cluster module using the community module from the registry (pinned version), wire all modules together in the environment root, deploy to dev and verify the complete infrastructure stack. Then create the production environment configuration with HA settings and validate it matches the production requirements.

Analogy🏏Cricket
🏏 Think of it like cricket: adding the EKS cluster from a pinned community module and wiring every module together in the environment root is like naming your full eleven and finalising the complete match-day setup once the ground and records are ready. Just as you pin the module version, you lock in a confirmed squad list rather than a shifting lineup — everyone knows the exact edition they're fielding. Assembling network, data, and cluster in one root config is like the captain bringing pitch, scorebook, and players together into a single coherent game plan. Then creating the production environment with HA settings — multiple NAT gateways, larger capacity — is like scaling up from the practice ground to a full stadium fixture with reserves and redundancy for a high-stakes final. Validating it matches the production requirements is the final kit and eligibility check before the big match. The payoff: a complete, verified infrastructure stack that runs in dev and stands up to production, like a squad drilled in the nets and ready for the final.
hcl
# modules/eks-cluster/main.tf — using community module
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"    # Pinned minor version

  cluster_name    = var.cluster_name
  cluster_version = "1.29"
  vpc_id          = var.vpc_id
  subnet_ids      = var.private_subnet_ids

  cluster_endpoint_public_access = true

  eks_managed_node_groups = {
    main = {
      instance_types = [var.node_instance_type]
      min_size       = var.min_nodes
      max_size       = var.max_nodes
      desired_size   = var.desired_nodes

      labels = {
        Environment = var.environment
        Project     = "cricketpulse"
      }
    }
  }

  enable_cluster_creator_admin_permissions = true
  tags = var.common_tags
}

output "cluster_endpoint"      { value = module.eks.cluster_endpoint }
output "cluster_name"          { value = module.eks.cluster_name }
output "node_security_group_id"{ value = module.eks.node_security_group_id }

# environments/production/main.tf — production config (key differences)
module "vpc" {
  source              = "../../modules/vpc"
  nat_gateway_count   = 2   # HA: one NAT per AZ
  # ... all other params same as dev with production CIDRs
}

module "rds" {
  source         = "../../modules/rds"
  instance_class = "db.r6g.large"  # Production: RDS optimised
  multi_az       = true            # HA: standby in second AZ
  storage_gb     = 100
  backup_retention_days = 7
  skip_final_snapshot   = false    # Production: keep final snapshot
  # ...
}

module "eks" {
  source             = "../../modules/eks-cluster"
  node_instance_type = "t3.large"  # Production: larger nodes
  min_nodes          = 3           # HA: always 3 nodes minimum
  max_nodes          = 10
  desired_nodes      = 3
  # ...
}

Evaluation Rubric

  • Module boundary correctness: each module (vpc, eks-cluster, rds, elasticache) exposes only the outputs needed by other modules and accepts only variables needed for its own resources — no variables are passed through modules unnecessarily.
  • Zero secrets in any file: `terraform plan` output shows no plaintext passwords or API keys, `*.tfvars` files contain no secrets, and `git log -- '*.tfvars' '*.tfstate'` shows no sensitive file was ever committed.
  • Environment parity: the dev and production root modules call identical modules with only size/HA parameters differing — no structural differences that would prevent a production deploy from following the same module call pattern.
  • Terraform plan idempotency: running `terraform plan` twice in succession against a deployed environment shows 'No changes' on the second run — confirming no resources have improper lifecycle configurations causing spurious diffs.
  • Security group correctness: `aws ec2 describe-security-groups` confirms RDS security group allows only port 5432 from the EKS node security group (not from 0.0.0.0/0) and the EKS node security group is not open to the internet.
  • CI/CD validation: the GitHub Actions plan job successfully posts a `terraform plan` output as a PR comment using OIDC authentication (no AWS_ACCESS_KEY_ID in Actions secrets), and the apply job requires and receives environment protection approval before running.
  • Drift detection: nightly `terraform plan -refresh-only -detailed-exitcode` returns exit code 0 (no drift) immediately after deployment — baseline drift detection passing is the pre-requisite for the schedule to be meaningful.

Extension Challenges: (1) Add a Helm provider and `helm_release` resource to the EKS module that installs the AWS Load Balancer Controller, enabling Kubernetes Ingress resources to provision ALBs automatically. (2) Configure Terraform Cloud as the backend instead of S3+DynamoDB — migrate state using `terraform state push` and use Terraform Cloud's built-in plan-in-VCS trigger mode to replace the GitHub Actions workflow. (3) Add a `kubernetes` provider to the root module that configures a CricketPulse Namespace and deploys a ServiceAccount with the correct IAM role annotation for IRSA (IAM Roles for Service Accounts), enabling CricketPulse API pods to access S3 and Secrets Manager without node-level IAM credentials.

Submit your capstone project

Checking submission status…
Final Exam unlocks when all 24 lessons are complete (24 left)
Lesson 24 of 24
0% complete