AWS Auto Scaling with Launch Templates
This week, I learned how to use AWS Auto Scaling Groups with Launch Templates in Terraform. This was a big step for me because now my applications can automatically grow or shrink based on traffic.
Before this, I built a three-tier application with a database (DNS-to-DB project). That project had 3 different applications with fixed EC2 instances and an RDS database. Now I wanted to learn something different, how to make my infrastructure scale automatically.
What Changed from My Previous Project?
In my DNS-to-DB project, I had:
- 3 applications (App1, App2, App3)
- 6 fixed EC2 instances (2 for each app)
- RDS MySQL database
- Path-based routing (
/app1,/app2,/)
In this new Auto Scaling project, I have:
- 1 application (App1)
- 2-5 EC2 instances that scale automatically
- No database (simplified)
- One simple route (
/*)
Why I simplified it: I wanted to focus only on learning Auto Scaling without the complexity of multiple apps and databases. This way, I can understand each part better.
What is a Launch Template?
A Launch Template is like a recipe for creating EC2 instances. Instead of creating instances one by one, I write the recipe once, and AWS uses it to create as many instances as needed.
My Launch Template Configuration
hcl
resource "aws_launch_template" "my_launch_template" {
name = "my_launch_template"
description = "My launch template"
image_id = data.aws_ami.amzlinux2.id
instance_type = var.instance_type
vpc_security_group_ids = [module.public_bastion_sg.security_group_id]
key_name = var.instance_keypair
user_data = filebase64("${path.module}/app1-install.sh")
ebs_optimized = true
update_default_version = true
block_device_mappings {
device_name = "/dev/sda1"
ebs {
volume_size = 20
volume_type = "gp2"
delete_on_termination = true
}
}
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "myasg"
}
}
}What this does:
- Uses Amazon Linux 2 AMI
- Creates t2.micro instances (free tier)
- Attaches my security group
- Runs app1-install.sh script on startup
- Creates 20GB storage
- Enables detailed monitoring
- Tags all instances with “myasg” name
Important note: I use filebase64() for user_data instead of file(). This is the correct way for Launch Templates because they need base64-encoded data.
What is an Auto Scaling Group?
An Auto Scaling Group (ASG) uses my Launch Template to create and manage instances automatically. It’s like having a smart manager that watches my application and decides when to add or remove servers.
My Auto Scaling Group Setup
hcl
resource "aws_autoscaling_group" "my_asg" {
name_prefix = "myasg_"
desired_capacity = 2
max_size = 5
min_size = 2
vpc_zone_identifier = module.vpc.public_subnets
target_group_arns = [module.alb.target_groups["mytg1"].arn]
health_check_type = "EC2"
launch_template {
id = aws_launch_template.my_launch_template.id
version = aws_launch_template.my_launch_template.latest_version
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
triggers = ["desired_capacity"]
}
tag {
key = "Owners"
value = "Web-Team"
propagate_at_launch = true
}
}Configuration explained:
- desired_capacity = 2: I want 2 instances running normally
- min_size = 2: Never go below 2 instances
- max_size = 5: Never go above 5 instances
- vpc_zone_identifier: Deploy in public subnets (both availability zones)
- target_group_arns: Connect to my ALB target group
- health_check_type: Use EC2 health checks
Instance refresh strategy:
- Uses “Rolling” strategy
- Keeps at least 50% of instances healthy during updates
- This means if I update my Launch Template, ASG will replace instances one by one without downtime
One big difference: In my DNS-to-DB project, I used private subnets for application servers. Here, I use public subnets. This is simpler for learning, but in production, I should use private subnets for better security.
Auto Scaling Policies: When to Scale?
This is the most interesting part! I configured three different ways to trigger scaling.
1. CPU-Based Scaling (Target Tracking)
hcl
resource "aws_autoscaling_policy" "avg_cpu_policy_greater_than_xx" {
name = "avg-cpu-policy-greater-than-xx"
policy_type = "TargetTrackingScaling"
autoscaling_group_name = aws_autoscaling_group.my_asg.id
estimated_instance_warmup = 180
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 50.0
}
}How this works:
- AWS watches the average CPU of all my instances
- If average CPU goes above 50%, it adds more instances
- If average CPU goes below 50%, it removes instances
- estimated_instance_warmup = 180: Wait 3 minutes before counting new instance metrics (because it takes time to start up)
Example scenario:
- I have 2 instances, each using 40% CPU → Average = 40% → No action
- Traffic increases, both instances now use 70% CPU → Average = 70% → AWS adds 1 more instance
- With 3 instances, average drops to 50% → Stays at 3 instances
- Traffic decreases, average drops to 30% → AWS removes 1 instance
2. Request Count-Based Scaling
hcl
resource "aws_autoscaling_policy" "alb_target_requests_greater_than_yy" {
name = "alb-target-requests-greater-than-yy"
policy_type = "TargetTrackingScaling"
autoscaling_group_name = aws_autoscaling_group.my_asg.id
estimated_instance_warmup = 120
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ALBRequestCountPerTarget"
resource_label = "${module.alb.arn_suffix}/${module.alb.target_groups["mytg1"].arn_suffix}"
}
target_value = 10.0
}
}How this works:
- AWS counts how many requests each instance receives
- Target: 10 requests per instance
- If each instance gets more than 10 requests, add more instances
- estimated_instance_warmup = 120: Wait 2 minutes for new instances
Example scenario:
- 2 instances, 15 requests per instance → Add 1 instance
- Now 3 instances, requests spread to ~7 per instance → No action
- Traffic grows to 40 total requests → 13 per instance → Add 1 more instance
Important detail: The resource_label connects this policy to my specific ALB and target group. I had to update this from my previous module version:
hcl
# Old way (didn't work with new module)
resource_label = "${module.alb.lb_arn_suffix}/${module.alb.target_group_arn_suffixes[0]}"
# New way (works with module 9.11.0)
resource_label = "${module.alb.arn_suffix}/${module.alb.target_groups["mytg1"].arn_suffix}"3. Scheduled Scaling
hcl
resource "aws_autoscaling_schedule" "increase_capacity_7am" {
scheduled_action_name = "increase-capacity-7am"
min_size = 2
max_size = 10
desired_capacity = 8
start_time = "2030-03-30T11:00:00Z"
recurrence = "00 09 * * *"
autoscaling_group_name = aws_autoscaling_group.my_asg.id
}
resource "aws_autoscaling_schedule" "decrease_capacity_5pm" {
scheduled_action_name = "decrease-capacity-5pm"
min_size = 2
max_size = 10
desired_capacity = 2
start_time = "2030-03-30T21:00:00Z"
recurrence = "00 21 * * *"
autoscaling_group_name = aws_autoscaling_group.my_asg.id
}How this works:
- At 9:00 AM UTC (7 AM EST): Scale to 8 instances
- At 9:00 PM UTC (5 PM EST): Scale down to 2 instances
- Happens every day automatically
- recurrence = “00 09 * * *”: Cron format (minute hour day month weekday)
Why this is useful:
- If I know my app gets more traffic during work hours, I can prepare in advance
- Save money by running fewer instances at night
- Don’t wait for CPU or requests to trigger scaling
Time zone note: AWS uses UTC time. I live in EST, so I need to convert:
- 7 AM EST = 11 AM UTC (in winter) or 12 PM UTC (in summer)
- I used 9 AM UTC in my example for simplicity
Email Notifications with SNS
I want to know when instances are added or removed, so I configured SNS (Simple Notification Service) to send me emails.
SNS Topic and Subscription
hcl
resource "aws_sns_topic" "myasg_sns_topic" {
name = "myasg-sns-topic-${random_pet.this.id}"
}
resource "aws_sns_topic_subscription" "myasg_sns_topic_subscription" {
topic_arn = aws_sns_topic.myasg_sns_topic.arn
protocol = "email"
endpoint = "rezachegini1994@gmail.com"
}
resource "aws_autoscaling_notification" "myasg_notifications" {
group_names = [aws_autoscaling_group.my_asg.id]
notifications = [
"autoscaling:EC2_INSTANCE_LAUNCH",
"autoscaling:EC2_INSTANCE_TERMINATE",
"autoscaling:EC2_INSTANCE_LAUNCH_ERROR",
"autoscaling:EC2_INSTANCE_TERMINATE_ERROR",
]
topic_arn = aws_sns_topic.myasg_sns_topic.arn
}What happens:
- SNS topic is created with a unique name (using random_pet)
- My email is subscribed to this topic
- Auto Scaling sends notifications to this topic
- I receive emails for:
- When a new instance launches successfully
- When an instance terminates
- If there’s an error launching an instance
- If there’s an error terminating an instance
Important: After running terraform apply, I need to check my email and confirm the SNS subscription. AWS sends a confirmation email and I must click the link to start receiving notifications.
Why I used random_pet: SNS topic names must be unique in my AWS account. random_pet.this.id generates a random name like “complete-hawk” or “happy-elephant”, making each topic name unique even if I create multiple environments.
Application Load Balancer Changes
In my DNS-to-DB project, I had complex routing with 3 different paths. Now it’s much simpler.
Old ALB Configuration (DNS-to-DB)
hcl
rules = {
myapp1-rule = {
priority = 10
path_pattern = { values = ["/app1*"] }
target_group = "mytg1"
}
myapp2-rule = {
priority = 20
path_pattern = { values = ["/app2*"] }
target_group = "mytg2"
}
myapp3-rule = {
priority = 30
path_pattern = { values = ["/*"] }
target_group = "mytg3"
}
}New ALB Configuration (Auto Scaling)
hcl
rules = {
myapp1-rule = {
actions = [{
type = "weighted-forward"
target_groups = [{
target_group_key = "mytg1"
weight = 1
}]
stickiness = {
enabled = true
duration = 3600
}
}]
conditions = [{
path_pattern = {
values = ["/*"]
}
}]
}
}What changed:
- Only ONE rule now
- Only ONE target group (mytg1)
- Catches all paths with
/* - Much simpler!
Important difference: I don’t manually attach EC2 instances to the target group anymore. The Auto Scaling Group handles this automatically. When ASG creates a new instance, it automatically registers it with the target group. When ASG terminates an instance, it automatically deregisters it.
Target Group Configuration
What I Learned
Technical Lessons
- Launch Templates vs. Launch Configurations: Launch Templates are newer and better. They support more features and can have versions.
- Target Tracking is smart: I don’t need to write complex rules. I just say “keep CPU at 50%” and AWS figures out when to scale.
- Warmup time matters: If I set it too short, ASG might add too many instances because new ones aren’t counted yet. If too long, scaling is slow.
- Multiple policies work together: I can have CPU-based AND request-based policies at the same time. AWS uses whichever triggers first.
- Public vs Private subnets: For learning, public subnets are okay. But in production, I should use private subnets with a NAT Gateway (like in my DNS-to-DB project).
- Health checks are critical: If my health check path is wrong, instances will be marked unhealthy and ASG will keep terminating and creating new ones (death spiral!).
Mistakes I Made
- Forgot to confirm SNS subscription: Deployed everything but didn’t click the email link. Wondered why I wasn’t getting notifications for 30 minutes!
- Used wrong resource_label format: Copied code from an old tutorial. Got errors because the ALB module outputs changed. Had to read the module documentation to fix it.
- Set warmup time too short (60 seconds): ASG added too many instances because new ones weren’t ready yet. Changed to 180 seconds and it worked better.
- Wrong timezone in scheduled actions: I used my local time (EST) instead of UTC. Scaling happened 4 hours earlier than I wanted!
- Target group health check path wrong: First I used
/as health check path, but my app only works on/app1/index.html. All instances showed unhealthy. Fixed by using correct path.
My Deployment Steps
For anyone who wants to try this:
bash
# 1. Clone or create the terraform files
cd terraform-manifests
# 2. Initialize Terraform
terraform init
# 3. Check what will be created
terraform plan
# 4. Deploy everything
terraform apply
# 5. Confirm SNS subscription (CHECK YOUR EMAIL!)
# 6. Test the application
curl https://asg-lt.rezaops.com/app1/index.html
# 7. Watch Auto Scaling in action
aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names myasg_xxx
# 8. Clean up when done (to avoid charges)
terraform destroyImportant: Don’t forget to run terraform destroy after testing, or you’ll get AWS charges! I learned this the hard way in my first month 😅