DEV Community

Mukami
Mukami

Posted on

Building a Scalable Web Application on AWS with EC2, ALB, and Auto Scaling using Terraform

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
  )
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)