Terraform in Practice #4 ALB and Auto Scaling: Launch Template, Target Group, and Scaling Policy
The instance from the previous part is a single machine: if it dies, that is the end, and it does not grow when traffic spikes. This part solves both problems at once. The ALB takes the entrance, and an Auto Scaling group (ASG) manages how many instances exist and whether they live or die. From a Terraform perspective, the lessons of this part are watching five resource types (launch template, ALB, target group, listener, ASG) get wired together by reference, and the experience of deleting an existing resource from the code.
Launch template: making the instance definition reusable #
An ASG needs a mold that says “launch instances according to this definition.” We move what we wrote in aws_instance last part into a launch template.
# compute.tf changes
resource "aws_launch_template" "web" {
name_prefix = "myapp-${var.env}-web-"
image_id = data.aws_ami.al2023.id
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.web.id]
user_data = base64encode(<<-EOF
#!/bin/bash
dnf install -y nginx
echo "myapp ${var.env} - $(hostname)" > /usr/share/nginx/html/index.html
systemctl enable --now nginx
EOF
)
lifecycle {
create_before_destroy = true
}
}The content is nearly identical, with two differences. The user_data must be wrapped in base64encode() (an API requirement of launch templates), and the name_prefix plus create_before_destroy pair goes here too. When the template changes, a new version is created and the ASG uses it starting from new instances — so the “user_data change means replacement” property from the previous part becomes, here, the raw material for rolling replacement.
ALB, target group, listener #
A load balancer is a combination of three resources: the ALB itself (the entrance), the target group (the bundle of servers behind it plus health checks), and the listener (the rule connecting the entrance to the bundle).
# lb.tf (new file)
resource "aws_lb" "main" {
name = "myapp-${var.env}"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [for s in aws_subnet.public : s.id]
}
resource "aws_lb_target_group" "web" {
name_prefix = "web-"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/"
healthy_threshold = 2
unhealthy_threshold = 3
}
lifecycle {
create_before_destroy = true
}
}
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.web.arn
}
}The ALB lands in the public subnets, wearing the ALB SG we prepared last part. The listener opens HTTP 80 only, for now. HTTPS needs a certificate, and we decided to handle that together with the domain in #10 — so today’s port 80 listener is scheduled to turn into a redirect at that point.
Auto Scaling group and scaling policy #
Now we tie the three together.
resource "aws_autoscaling_group" "web" {
name_prefix = "myapp-${var.env}-web-"
min_size = 2
max_size = 4
desired_capacity = 2
vpc_zone_identifier = [for s in aws_subnet.private : s.id]
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
lifecycle {
ignore_changes = [desired_capacity]
}
}
resource "aws_autoscaling_policy" "cpu" {
name = "cpu-target-tracking"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 60
}
}The design decisions worth noting:
- min 2, private subnets across two AZs: the minimum layout where the service survives one instance dying or one AZ having trouble.
- health_check_type = “ELB”: not just the EC2 status checks — a failing target group health check also counts as grounds for replacement. If nginx dies on an instance, the ASG swaps it out on its own.
- ignore_changes = [desired_capacity]: exactly the case previewed in Basics #7. It keeps the next apply from resetting to 2 the instance count that the scaling policy adjusted.
- Target tracking policy: it scales out and in on its own to hold average CPU at 60%. It is more declarative than hand-building step alarms, which matches Terraform’s grain.
Deleting the single instance: code removal is resource removal #
Last part’s aws_instance.web has finished its job. In IaC, removing a resource is not a command — it is deleting code. Remove the aws_instance block from compute.tf and look at the plan: “1 to destroy” picks out exactly that one instance as the removal target. This single plan — creating the new resources while removing the old one — summarizes this entire part’s changes, so it is the first moment in the series where reading the plan carefully from top to bottom really pays off.
Once apply finishes, we connect to the ALB address added to terraform output.
output "alb_dns_name" {
value = aws_lb.main.dns_name
}The browser shows a myapp dev - ip-10-0-11-x... response, and repeated refreshes make the hostname alternate. Two instances answering in turn — the first working demo of this series. The ALB and instances are running, so if you are stopping here for the day, do not forget to destroy.
Recap #
What we covered in this post:
- We moved the instance definition into a launch template. The user_data needs base64 encoding, and template changes become new versions — the raw material for rolling replacement
- An ALB is a combination of three resources: the ALB itself, the target group, and the listener. HTTPS gets attached in #10 along with the domain
- The ASG spans two AZs with min 2, and health_check_type ELB gives it application-level self-healing
- ignore_changes on desired_capacity keeps the scaling policy and Terraform from fighting each other
- Removing a resource means deleting code. The plan that removes the single instance walked us through the IaC removal flow
In the next post (#5 RDS and S3), we build the data layer: a PostgreSQL RDS in the private subnets, an S3 bucket for static assets, and the safety locks that go on data resources.