Loading
Reza Chegini

Junior DevOps Engineer

Junior Cloud Engineer

Junior Site Reliability Engineer

Software Engineer

Backend Developer

Reza Chegini

Junior DevOps Engineer

Junior Cloud Engineer

Junior Site Reliability Engineer

Software Engineer

Backend Developer

Blog Post

AWS Auto Scaling with Launch Templates

October 14, 2025 AWS, DevOps, Infrastructure, Terraform
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:

  1. SNS topic is created with a unique name (using random_pet)
  2. My email is subscribed to this topic
  3. Auto Scaling sends notifications to this topic
  4. 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

  1. Launch Templates vs. Launch Configurations: Launch Templates are newer and better. They support more features and can have versions.
  2. 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.
  3. 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.
  4. Multiple policies work together: I can have CPU-based AND request-based policies at the same time. AWS uses whichever triggers first.
  5. 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).
  6. 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

  1. Forgot to confirm SNS subscription: Deployed everything but didn’t click the email link. Wondered why I wasn’t getting notifications for 30 minutes!
  2. 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.
  3. 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.
  4. Wrong timezone in scheduled actions: I used my local time (EST) instead of UTC. Scaling happened 4 hours earlier than I wanted!
  5. 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 destroy

Important: 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 😅

Tags:
Write a comment