What You'll Build
You will write the Terraform configuration for all five infrastructure components of the capstone project, composing the VPC module from M2 with new ALB, ASG, RDS and S3 configurations. Each component uses production patterns: the ALB with HTTPS listener and ACM certificate, the ASG with launch template and health check, the RDS with encryption and Multi-AZ, and the S3 bucket with all security settings. You will also write the module composition in the root configuration and verify that 'terraform plan' shows the expected resources and 'Checkov' passes all HIGH severity checks.
Analogy🏏Cricket
🏏 Think of it like cricket: You are designing the BCCI's standard cricket ground specification module — a parameterisable blueprint that can be instantiated for any venue in the country without redrawing it from scratch. The blueprint fixes the non-negotiables every certified ground shares: a boundary perimeter (the VPC and its CIDR), spectator zones with public access (public subnets with an Internet Gateway route), and restricted player-and-officials zones behind accreditation checks (private subnets routing through NAT). What varies per venue arrives as parameters: how many stands to build (var.azs and subnet counts), whether this is a full international stadium or a modest district ground (var.single_nat_gateway as the cost lever — one shared service corridor instead of one per stand), and the venue's name and signage (tags). Wankhede and a Ranchi district ground are instantiated from the same drawing with different inputs — and when the board later improves the blueprint (adds flow logs, tightens NACLs), every venue inherits the improvement on its next renovation (module version bump) instead of each ground hand-patching its own architecture. That is the entire economics of module authorship: design once, rigorously, then stamp out consistent grounds forever.
hcl
# environments/production/main.tf — Capstone infrastructure
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
random = { source = "hashicorp/random", version = "~> 3.5" }
}
backend "s3" {
bucket = var.state_bucket
key = "cricket-analytics/production/terraform.tfstate"
region = "ap-south-1"
dynamodb_table = "cricket-terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "cricket-analytics"
Environment = var.environment
ManagedBy = "terraform"
}
}
}
# ── VPC Module (reused from M2) ───────────────────────────────────────────────
module "vpc" {
source = "../../modules/vpc"
name = "cricket-${var.environment}"
vpc_cidr = var.vpc_cidr
azs = var.azs
enable_nat_gateway = true
single_nat_gateway = !local.is_production
enable_flow_logs = local.is_production
tags = local.common_tags
}
# ── ALB with HTTPS listener ───────────────────────────────────────────────────
resource "aws_lb" "api" {
name = "cricket-${var.environment}-alb"
internal = false
load_balancer_type = "application"
subnets = module.vpc.public_subnet_ids
security_groups = [aws_security_group.alb.id]
drop_invalid_header_fields = true
# checkov:skip=CKV_AWS_91:Access logging requires additional S3 bucket configuration
tags = { Name = "cricket-${var.environment}-alb" }
}
resource "aws_lb_target_group" "api" {
name = "cricket-${var.environment}-tg"
port = 80
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "instance"
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
interval = 30
timeout = 10
}
}
data "aws_acm_certificate" "cricket" {
domain = "*.cricket-analytics.io"
statuses = ["ISSUED"]
most_recent = true
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.api.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = data.aws_acm_certificate.cricket.arn
default_action { type = "forward"; target_group_arn = aws_lb_target_group.api.arn }
}
resource "aws_lb_listener" "http_redirect" {
load_balancer_arn = aws_lb.api.arn
port = 80
protocol = "HTTP"
default_action { type = "redirect"; redirect { port = "443"; protocol = "HTTPS"; status_code = "HTTP_301" } }
}
# ── EC2 Auto Scaling Group ────────────────────────────────────────────────────
resource "aws_launch_template" "api" {
name_prefix = "cricket-${var.environment}-api-"
image_id = data.aws_ami.al2023.id
instance_type = local.is_production ? "t3.medium" : "t3.micro"
vpc_security_group_ids = [aws_security_group.app.id]
iam_instance_profile { arn = aws_iam_instance_profile.app.arn }
metadata_options { http_tokens = "required"; http_put_response_hop_limit = 1 }
ebs_optimized = true
block_device_mappings {
device_name = "/dev/xvda"
ebs { volume_type = "gp3"; volume_size = 20; encrypted = true; delete_on_termination = true }
}
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, {
Name = "cricket-${var.environment}-api"
Role = "api-server"
AnsibleManaged = "true"
})
}
lifecycle { create_before_destroy = true }
}
resource "aws_autoscaling_group" "api" {
name = "cricket-${var.environment}-asg"
vpc_zone_identifier = module.vpc.private_subnet_ids
target_group_arns = [aws_lb_target_group.api.arn]
health_check_type = "ELB"
health_check_grace_period = 120
min_size = local.is_production ? 2 : 1
max_size = local.is_production ? 6 : 2
desired_capacity = local.is_production ? 2 : 1
launch_template { id = aws_launch_template.api.id; version = "$Latest" }
lifecycle {
ignore_changes = [desired_capacity] # Managed by scaling policies
create_before_destroy = true
}
tag { key = "Name"; value = "cricket-${var.environment}-asg"; propagate_at_launch = false }
}
# ── RDS PostgreSQL Multi-AZ ───────────────────────────────────────────────────
resource "aws_db_instance" "analytics" {
identifier = "cricket-${var.environment}-db"
engine = "postgres"
engine_version = "16.1"
instance_class = local.is_production ? "db.r5.large" : "db.t3.micro"
allocated_storage = local.is_production ? 200 : 20
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
db_subnet_group_name = aws_db_subnet_group.analytics.name
vpc_security_group_ids = [aws_security_group.rds.id]
multi_az = local.is_production
publicly_accessible = false
username = "cricket_admin"
password = random_password.db.result
backup_retention_period = local.is_production ? 14 : 1
deletion_protection = local.is_production
skip_final_snapshot = !local.is_production
performance_insights_enabled = local.is_production
tags = { Name = "cricket-${var.environment}-db" }
lifecycle {
prevent_destroy = true # REQUIRED for capstone grading
ignore_changes = [password]
}
}
# ── S3 data bucket ────────────────────────────────────────────────────────────
resource "aws_s3_bucket" "data" {
bucket = "cricket-data-${var.environment}-${data.aws_caller_identity.current.account_id}"
lifecycle { prevent_destroy = true } # REQUIRED for capstone grading
}
# [versioning, encryption, public_access_block, lifecycle_configuration resources omitted for brevity]
# ── Supporting resources ───────────────────────────────────────────────────────
resource "aws_kms_key" "rds" {
description = "Cricket ${var.environment} RDS encryption"
enable_key_rotation = true
deletion_window_in_days = 14
}
resource "random_password" "db" {
length = 32; special = true; override_special = "!#$%&*-_=+<>?"
}
resource "aws_secretsmanager_secret" "db_password" {
name = "/cricket/${var.environment}/db-password"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db.result
lifecycle { ignore_changes = [secret_string] }
}
data "aws_ami" "al2023" {
most_recent = true; owners = ["amazon"]
filter { name = "name"; values = ["al2023-ami-*-x86_64"] }
filter { name = "state"; values = ["available"] }
}
data "aws_caller_identity" "current" {}
locals {
is_production = var.environment == "production"
common_tags = { Environment = var.environment }
}Analogy🏏Cricket
🏏 Think of it like cricket: Notice what the composition in main.tf actually does with the modules: nothing is built twice. The VPC module you authored in M2 is instantiated here unchanged — the rehearsed powerplay routine executed again in a bigger match, not re-invented for it — and every new component plugs into its published outputs: the ALB takes module.vpc.public_subnet_ids the way the fielding plan takes the ground dimensions as given, the ASG takes the private subnet IDs, the RDS subnet group takes the database tier's. That chain of references IS the architecture — Terraform reads it and derives the entire build order (the graph knows subnets precede the ALB, and the ALB's target group precedes the ASG that registers into it) with no explicit sequencing from you, the way a competent operations team derives the setup schedule from the venue drawings rather than being told step by step. The production patterns are non-negotiable for the same reasons their cricket counterparts are: Multi-AZ RDS is the synchronised duplicate record room in a second building (a lost AZ loses no data), create_before_destroy on the launch template is the replacement keeper drilled before the incumbent leaves (no capacity gap during updates), and prevent_destroy on the database is heritage protection on the trophy room — the one demolition that must never be a side effect of a routine renovation.
- Compose infrastructure from focused modules (VPC module) and direct resource blocks (ALB, ASG, RDS) — modules for reusable patterns, direct resources for component-specific configuration.
- The ASG's desired_capacity has 'ignore_changes = [desired_capacity]' — Auto Scaling manages runtime count; Terraform manages the min/max bounds and launch template.
- RDS and S3 have 'lifecycle { prevent_destroy = true }' — this is the mandatory capstone requirement demonstrating stateful resource protection; the grader will verify terraform destroy fails for these resources.
- The ALB listener uses the most current TLS policy ('ELBSecurityPolicy-TLS13-1-2-2021-06') — always check the AWS documentation for the current recommended policy as it changes when older TLS versions are deprecated.