What You'll Build
You will author a reusable `cricketpulse-cluster` Terraform module that provisions a complete application server cluster: an autoscaling group of EC2 instances, an Application Load Balancer, target groups, and necessary security groups. The module has a clean variable interface (instance type, min/max replicas, VPC inputs), meaningful outputs (load balancer DNS, ASG name), and an `examples/` directory with a basic usage example. You will then call this module from a root configuration for two environments (dev and production) and observe how the same module produces different infrastructure based on input values.
Prerequisites
- Terraform 1.5+ installed with AWS provider configured.
- AWS credentials with EC2, ELB, and IAM permissions (or LocalStack for local testing).
- Lessons 13–15 completed — understanding of module authoring, versioning, and composition.
- Completion of Lesson 12's networking configuration, or create a VPC and subnets manually to reference.
- Familiarity with AWS Application Load Balancer and EC2 autoscaling concepts.
Setup & Project Structure
Create the module directory structure following Terraform conventions, then scaffold all required files before implementing the resource logic.
mkdir -p cricketpulse-modules/modules/cricketpulse-cluster/examples/basic
cd cricketpulse-modules
# Create module files
touch modules/cricketpulse-cluster/{variables.tf,main.tf,outputs.tf,versions.tf,README.md}
touch examples/basic/{main.tf,outputs.tf}
# Initialize the root configuration for calling the module
cat > main.tf << 'EOF'
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = var.aws_region }
EOF
cat > variables.tf << 'EOF'
variable "aws_region" { type = string; default = "ap-south-1" }
variable "environment" { type = string }
EOF
terraform init
echo 'Project structure created'Step 1 — Foundation
Define the module's variables, outputs, and versions files — the module's interface contract. Getting the interface right first avoids later breaking changes that affect callers.
cat > modules/cricketpulse-cluster/variables.tf << 'EOF'
variable "environment" {
type = string
description = "Deployment environment (dev, staging, production)"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "vpc_id" {
type = string
description = "VPC ID to deploy the cluster into"
}
variable "subnet_ids" {
type = list(string)
description = "List of subnet IDs for the autoscaling group (private) and ALB (public)"
}
variable "alb_subnet_ids" {
type = list(string)
description = "List of public subnet IDs for the ALB"
}
variable "instance_type" {
type = string
description = "EC2 instance type for API servers"
default = "t3.medium"
}
variable "min_size" {
type = number
description = "Minimum number of instances in the autoscaling group"
default = 2
}
variable "max_size" {
type = number
description = "Maximum number of instances in the autoscaling group"
default = 10
}
variable "desired_capacity" {
type = number
description = "Desired number of instances"
default = 2
}
variable "ami_id" {
type = string
description = "AMI ID for CricketPulse API servers"
}
variable "project" {
type = string
description = "Project name for resource naming and tagging"
default = "cricketpulse"
}
EOF
cat > modules/cricketpulse-cluster/versions.tf << 'EOF'
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0.0, < 6.0.0"
}
}
}
EOF
cat > modules/cricketpulse-cluster/outputs.tf << 'EOF'
output "alb_dns_name" {
value = aws_lb.main.dns_name
description = "DNS name of the Application Load Balancer"
}
output "alb_arn" {
value = aws_lb.main.arn
description = "ARN of the Application Load Balancer"
}
output "asg_name" {
value = aws_autoscaling_group.main.name
description = "Name of the EC2 autoscaling group"
}
output "security_group_id" {
value = aws_security_group.api.id
description = "Security group ID for the API servers"
}
EOFStep 2 — Core Logic
Implement `main.tf` with the security group, launch template, autoscaling group, and Application Load Balancer resources. These are the core compute and network resources the module encapsulates.
cat > modules/cricketpulse-cluster/main.tf << 'EOF'
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
# Security group for API servers
resource "aws_security_group" "api" {
name = "${local.name_prefix}-api-sg"
description = "Security group for CricketPulse API servers"
vpc_id = var.vpc_id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "Allow traffic from ALB"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, { Name = "${local.name_prefix}-api-sg" })
}
# Security group for ALB
resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb-sg"
description = "Security group for CricketPulse ALB"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, { Name = "${local.name_prefix}-alb-sg" })
}
# Launch template
resource "aws_launch_template" "api" {
name_prefix = "${local.name_prefix}-api-"
image_id = var.ami_id
instance_type = var.instance_type
network_interfaces {
security_groups = [aws_security_group.api.id]
}
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, { Name = "${local.name_prefix}-api" })
}
lifecycle { create_before_destroy = true }
}
# Autoscaling Group
resource "aws_autoscaling_group" "main" {
name = "${local.name_prefix}-asg"
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
vpc_zone_identifier = var.subnet_ids
target_group_arns = [aws_lb_target_group.api.arn]
launch_template {
id = aws_launch_template.api.id
version = "$Latest"
}
tag {
key = "Environment"
value = var.environment
propagate_at_launch = true
}
}
# Application Load Balancer
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.alb_subnet_ids
tags = merge(local.common_tags, { Name = "${local.name_prefix}-alb" })
}
resource "aws_lb_target_group" "api" {
name = "${local.name_prefix}-api-tg"
port = 3000
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
EOFStep 3 — Integration & Enhancement
Create the examples directory with a basic usage example and call the module from the root configuration for both dev and production environments.
# examples/basic/main.tf — complete working example
cat > modules/cricketpulse-cluster/examples/basic/main.tf << 'EOF'
provider "aws" { region = "ap-south-1" }
data "aws_vpc" "default" { default = true }
data "aws_subnets" "default" {
filter { name = "vpc-id"; values = [data.aws_vpc.default.id] }
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter { name = "name"; values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }
}
module "cluster" {
source = "../.." # Points to module root
environment = "example"
vpc_id = data.aws_vpc.default.id
subnet_ids = data.aws_subnets.default.ids
alb_subnet_ids = data.aws_subnets.default.ids
ami_id = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
min_size = 1
max_size = 3
desired_capacity = 1
}
output "alb_dns" { value = module.cluster.alb_dns_name }
EOF
# Root: call module for two environments
cat >> main.tf << 'EOF'
data "aws_vpc" "cricketpulse" {
tags = { Name = "cricketpulse-${var.environment}" }
}
data "aws_subnets" "private" {
filter { name = "vpc-id"; values = [data.aws_vpc.cricketpulse.id] }
tags = { Tier = "private" }
}
data "aws_subnets" "public" {
filter { name = "vpc-id"; values = [data.aws_vpc.cricketpulse.id] }
tags = { Tier = "public" }
}
data "aws_ami" "ubuntu" {
most_recent = true; owners = ["099720109477"]
filter { name = "name"; values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }
}
module "cricketpulse_cluster" {
source = "./modules/cricketpulse-cluster"
environment = var.environment
vpc_id = data.aws_vpc.cricketpulse.id
subnet_ids = data.aws_subnets.private.ids
alb_subnet_ids = data.aws_subnets.public.ids
ami_id = data.aws_ami.ubuntu.id
instance_type = var.environment == "production" ? "m5.large" : "t3.small"
min_size = var.environment == "production" ? 3 : 1
max_size = var.environment == "production" ? 20 : 3
desired_capacity = var.environment == "production" ? 3 : 1
}
output "api_endpoint" {
value = "http://${module.cricketpulse_cluster.alb_dns_name}"
}
EOFStep 4 — Testing & Verification
Validate the module structure and run plans for both environments to verify the module produces different infrastructure configurations based on the environment variable.
# Validate module structure
terraform validate
# Plan dev environment
terraform plan -var="environment=dev"
# Expected: small instance type, 1 replica, all tagged 'dev'
# Plan production environment
terraform plan -var="environment=production"
# Expected: m5.large instances, 3 replicas, all tagged 'production'
# Verify module outputs are accessible
terraform plan -var="environment=dev" 2>&1 | grep 'alb_dns_name'
# Changes to outputs: api_endpoint = (known after apply)
# Test the example independently
cd modules/cricketpulse-cluster/examples/basic
terraform init
terraform plan # Should work with default VPC
# Validate the module can be consumed from a subdirectory
cd ../../../../
terraform -chdir=modules/cricketpulse-cluster/examples/basic validate
echo 'Module exercise complete!'Warning: The `create_before_destroy = true` lifecycle rule on the launch template is critical for zero-downtime scaling group updates. Without it, Terraform tries to delete the old launch template before creating the new one — but the autoscaling group still references the old template, causing a dependency error. `create_before_destroy` tells Terraform to create the new template first, update the ASG to reference it, then delete the old one. This pattern applies to any resource that other resources reference and that might need to be replaced.
Extension Challenge: Add a `health_check_path` variable to the module (default: `/health`) and use it in the target group health check. Then add a `tags` variable of type `map(string)` with default `{}` and merge it with `local.common_tags` on every resource: `tags = merge(local.common_tags, var.tags, { Name = ... })`. This makes the module generic enough that callers can add arbitrary tags without modifying the module internals — a critical capability for organisations with mandatory tagging requirements.
- Create the module's variable and output interfaces (variables.tf, outputs.tf) before implementing resources — the interface contract is more important than the implementation details.
- The `versions.tf` file in a module declares minimum Terraform and provider version requirements — callers will fail early with a clear error if they use an incompatible version.
- Module resource state is namespaced as `module.<name>.<resource_type>.<resource_name>` — calling the same module twice with different names creates separate, non-conflicting state keys.
- Use `create_before_destroy = true` on launch templates and other resources that are referenced by other resources and may need replacement — prevents dependency errors during updates.
- The `examples/basic` directory serves as the module's working documentation and can be used for manual integration testing with `terraform plan` from the examples directory.
- Conditional expressions in the caller (`instance_type = var.environment == "production" ? "m5.large" : "t3.small"`) enable environment-specific sizing without requiring the module to know about environment-specific logic.