From Static Website to Dynamic, Auto-Scaling Infrastructure
Day 26 of the 30-Day Terraform Challenge — and today I graduated from static to dynamic infrastructure.
Yesterday I deployed a static website on S3 with CloudFront. Today I built something that actually thinks for itself.
A web application that grows when traffic increases and shrinks when it drops. All without human intervention. All managed through Terraform.
The Architecture
Internet → ALB (Port 80) → Auto Scaling Group → EC2 Instances (2-4)
↓
CloudWatch Alarms
↓
Scale Out at 70% CPU
Scale In at 30% CPU
- Application Load Balancer distributes traffic across instances
- Auto Scaling Group maintains desired instance count (2-4)
- Launch Template defines instance configuration
- CloudWatch Alarms trigger scaling based on CPU utilization
- Three Terraform modules keep everything DRY and reusable
The Project Structure
day26-scalable-web-app/
├── modules/
│ ├── ec2/ # Launch Template + Security Group
│ ├── alb/ # Load Balancer + Target Group
│ └── asg/ # Auto Scaling Group + Scaling Policies
├── envs/
│ └── dev/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── backend.tf
└── provider.tf
Each module has a single responsibility. The EC2 module doesn't know about the ALB. The ALB module doesn't know about scaling. The ASG module connects them both.
The EC2 Module (Launch Template)
# modules/ec2/main.tf
resource "aws_launch_template" "web" {
name_prefix = "web-lt-${var.environment}-"
image_id = var.ami_id
instance_type = var.instance_type
user_data = base64encode(<<-USERDATA
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
echo "<h1>Environment: ${var.environment}</h1>
<p>Instance ID: $(curl -s http://169.254.169.254/latest/meta-data/instance-id)</p>"
> /var/www/html/index.html
USERDATA
)
}
The launch template defines what every instance looks like: AMI, instance type, security groups, and the user data script that runs at boot.
The ALB Module (Load Balancer)
# modules/alb/main.tf
resource "aws_lb" "web" {
name = "${var.name}-alb-${var.environment}"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.subnet_ids
}
resource "aws_lb_target_group" "web" {
name = "${var.name}-tg-${var.environment}"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/"
healthy_threshold = 2
unhealthy_threshold = 2
}
}
The ALB receives traffic and forwards it to healthy instances in the target group. The health check ensures traffic only goes to instances that actually respond.
The ASG Module (Auto Scaling)
# modules/asg/main.tf
resource "aws_autoscaling_group" "web" {
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
target_group_arns = var.target_group_arns
launch_template {
id = var.launch_template_id
version = "$Latest"
}
health_check_type = "ELB"
}
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
alarm_name = "web-cpu-high-${var.environment}"
threshold = var.cpu_scale_out_threshold # 70%
alarm_actions = [aws_autoscaling_policy.scale_out.arn]
}
resource "aws_autoscaling_policy" "scale_out" {
scaling_adjustment = 1
}
The ASG maintains the desired number of instances. When CPU exceeds 70%, the CloudWatch alarm triggers the scale-out policy, adding one instance. When CPU drops below 30%, it scales in.
How Modules Collaborate
module.ec2 (Launch Template ID) ──► module.asg
│
module.alb (Target Group ARN) ──────► module.asg
The calling configuration wires everything together:
# envs/dev/main.tf
module "ec2" {
source = "../../modules/ec2"
ami_id = var.ami_id
environment = var.environment
}
module "alb" {
source = "../../modules/alb"
name = var.app_name
vpc_id = var.vpc_id
subnet_ids = var.public_subnet_ids
}
module "asg" {
source = "../../modules/asg"
launch_template_id = module.ec2.launch_template_id
target_group_arns = [module.alb.target_group_arn]
min_size = 1
max_size = 4
desired_capacity = 2
}
The target_group_arns input is critical. Without it, instances would launch but never receive traffic. The ALB wouldn't know they exist.
The Deployment
$ terraform apply -auto-approve
Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
Outputs:
alb_url = "http://web-challenge-day26-alb-dev-1419490799.eu-north-1.elb.amazonaws.com"
The Result
ALB URL: http://web-challenge-day26-alb-dev-1419490799.eu-north-1.elb.amazonaws.com
What you see:
Scalable Web App — Environment: dev
Instance ID: i-0abcd1234efgh5678
Deployed with Terraform on Day 26!
What happens behind the scenes:
- 2 instances initially (desired_capacity = 2)
- When CPU hits 70%, CloudWatch triggers scale-out → 3 instances
- When CPU drops to 30%, scale-in triggers → back to 2 instances
Why This Matters
Before: A single EC2 instance. If it crashes, the site goes down. If traffic spikes, users see errors.
After: Auto Scaling Group with 2-4 instances. If one crashes, the ASG replaces it. If traffic spikes, the ASG adds capacity automatically.
The business value: You don't over-provision (paying for idle servers) and you don't under-provision (losing users during spikes).
What I Learned
Module composition > monolithic config. Three small modules are easier to understand, test, and reuse than one giant file.
health_check_type = "ELB" is critical. Without it, the ASG only checks if the EC2 instance is running, not if the application is responding.
CloudWatch alarms + scaling policies = auto-scaling. The alarm detects the condition. The policy executes the action.
The target group is the bridge. The ALB sends traffic to the target group. The ASG registers instances with the target group. Without this connection, nothing works.
Clean Up
terraform destroy -auto-approve
Always destroy test infrastructure to avoid unexpected AWS charges.
The Bottom Line
Today I built infrastructure that adapts to traffic. No manual scaling. No over-provisioning. No downtime during spikes.
Three modules working together: EC2 for instance configuration, ALB for traffic distribution, ASG for scaling.
This is what production-grade infrastructure looks like.
P.S. The moment I saw CloudWatch trigger a scale-out and a new instance appear in the target group, I understood why teams love auto scaling. It's not magic. It's just well-designed infrastructure.
Top comments (0)