⚖️ Application Load Balancer + Auto Scaling Group on AWS
So far in this series, we've built a custom VPC with public/private subnets and added a NAT Gateway for outbound access. Now it's time to put that network to real use: deploying a highly available, self-healing web tier using an Application Load Balancer (ALB) and an Auto Scaling Group (ASG).
By the end of this post, you'll have multiple EC2 instances running in private subnets, sitting behind a public-facing load balancer, automatically distributing traffic — and automatically replacing any instance that fails.
🧠 The Core Pattern
This is one of the most important architecture patterns in AWS:
Internet → ALB (public subnet) → Target Group → EC2 Instances (private subnet)
- The ALB is the only internet-facing component — it lives in the public subnet.
- The EC2 instances never touch the internet directly — they live in the private subnet, reachable only through the ALB.
- This is a defense-in-depth principle: never expose more than you have to. Even if someone discovers an instance's private IP, there's no route in from outside the VPC.
Traffic flow, step by step:
- A user hits the ALB's DNS name
- The ALB (in the public subnet, reachable via the Internet Gateway) receives the request
- The ALB forwards it internally — over the VPC's private network — to a healthy instance in the private subnet, via the Target Group
- The instance responds back through the same private path
🖼️ Architecture
Internet
│
▼
┌──────────────────┐
│ demo-alb │ (Public Subnets)
│ (alb-sg: 80 open) │
└─────────┬─────────┘
│
▼
┌──────────────────┐
│ demo-tg │
│ (Target Group) │
└─────────┬─────────┘
│
┌───────────────┴────────────────┐
▼ ▼
EC2 Instance (private) EC2 Instance (private)
instance-sg: 80 from alb-sg instance-sg: 80 from alb-sg
(Auto Scaling Group: demo-asg, desired 2, min 2, max 4)
🧩 A Key Design Choice: No Nginx, No Internet Needed
Our private subnets have no NAT Gateway, meaning the instances have no outbound internet access. A typical yum install nginx in User Data would fail here, since nginx isn't preinstalled and package repos are external.
Instead, we'll use Python's built-in HTTP server, which ships with Amazon Linux 2023 out of the box — no internet required. This keeps the whole demo self-contained.
💡 If your use case later needs real outbound access (installing packages, calling external APIs), add a NAT Gateway in the public subnet and route the private subnet's
0.0.0.0/0traffic through it — see my previous NAT Gateway blog here for the full walkthrough.
✅ Prerequisites
- A custom VPC with two public subnets and two private subnets, each pair spread across two Availability Zones (ALB requires subnets in 2+ AZs)
- An Internet Gateway attached to the VPC
The Build Order
We'll go: Security Groups → Target Group → Launch Template → ALB → Auto Scaling Group
Step 1: Create Two Security Groups
A) ALB Security Group
- EC2 Console → Security Groups → Create Security Group
- Name:
alb-sg, Description: "ALB security group", VPC: your demo VPC -
Inbound rules: Add rule → Type: HTTP, Port 80, Source: Anywhere-IPv4 (
0.0.0.0/0) - Outbound rules: leave default (all traffic allowed)
- Create
B) Instance Security Group
- Create another security group
- Name:
instance-sg, Description: "EC2 instance SG", same VPC -
Inbound rules: Add rule → Type: HTTP, Port 80, Source: Custom → select
alb-sg(not a CIDR range — this restricts traffic to only what's coming from the ALB) - Create
This is the crucial detail: instances only accept HTTP traffic that's coming from the load balancer, never directly from the internet.
Step 2: Create the Target Group
- EC2 Console → Target Groups → Create Target Group
- Target type: Instances
- Name:
demo-tg - Protocol: HTTP, Port: 80
- VPC: your demo VPC
-
Health checks:
- Protocol: HTTP, Path:
/ - Advanced settings: Healthy threshold
2, Unhealthy threshold2, Timeout5, Interval10, Success codes200
- Protocol: HTTP, Path:
- Next → skip "Register targets" (the ASG will handle this automatically) → Create target group
Step 3: Create the Launch Template
- EC2 Console → Launch Templates → Create Launch Template
- Name:
demo-lt - AMI: Amazon Linux 2023
- Instance type:
t3.micro - Key pair: optional — only needed if you want SSH access for debugging
-
Network settings: leave subnet blank (the ASG assigns subnets); Security groups: select
instance-sg - Advanced details → User data:
#!/bin/bash
mkdir -p /var/www/demo
cat <<EOF > /var/www/demo/index.html
<html><body><h1>Hello from $(hostname -f)</h1></body></html>
EOF
cd /var/www/demo
nohup python3 -m http.server 80 > /var/log/demo-server.log 2>&1 &
This serves a page showing the instance's hostname — so when you refresh the ALB's URL repeatedly, you can literally watch it load-balance across different instances.
- Create the template
Auto Scaling guidance checkbox: In the wizard, check "Provide guidance to help me set up a template for use with EC2 Auto Scaling." It doesn't restrict anything functionally — it just hides/discourages settings that don't make sense for ASG-launched instances (like picking a specific subnet or attaching a public IP), and nudges you toward leaving those to the ASG. Since our instances live in private subnets with no public IP needed, this is exactly the right guidance.
Step 4: Create the Application Load Balancer
- EC2 Console → Load Balancers → Create Load Balancer → Application Load Balancer
- Name:
demo-alb - Scheme: Internet-facing
- IP address type: IPv4
- Network mapping: select your VPC → check both public subnets (one per AZ — required, since the ALB needs subnets in 2+ Availability Zones)
-
Security groups: select
alb-sg(remove any default SG that gets auto-added) -
Listeners and routing: Listener HTTP:80 → Default action: Forward to → select
demo-tg - Create
Wait a minute or two for the state to become Active.
Step 5: Create the Auto Scaling Group
- EC2 Console → Auto Scaling Groups → Create Auto Scaling Group
- Name:
demo-asg - Launch template: select
demo-lt→ Next - VPC: your demo VPC → Availability Zones and subnets: select both private subnets (one per AZ)
- Next → Attach to an existing load balancer → Choose from your load balancer target groups → select
demo-tg - Health checks: turn ON "Turn on Elastic Load Balancing health checks" — without this, the ASG only checks raw EC2 status, not whether your app is actually responding
- Health check grace period:
60seconds (gives the instance time to boot before checks start counting against it) - Next → Group size:
- Desired capacity:
2 - Minimum:
2 - Maximum:
4
- Desired capacity:
- (Optional) Add a scaling policy like "Target tracking on CPU 50%," or skip for now
- Review → Create Auto Scaling group
Step 6: Verify
- Wait ~2–3 minutes for instances to launch and pass health checks
-
EC2 Console → Target Groups →
demo-tg→ Targets tab — both instances should show healthy -
EC2 Console → Load Balancers →
demo-alb— copy the DNS name (something likedemo-alb-123456789.us-east-1.elb.amazonaws.com) - Paste that DNS name into your browser and refresh a few times — you should see the hostname in the page change between instances as the ALB load-balances requests
🔍 Quick Recap
| Resource | Purpose |
|---|---|
alb-sg |
Allows HTTP from the internet, attached to the ALB |
instance-sg |
Allows HTTP only from alb-sg, attached to instances |
demo-tg |
Target group routing traffic to healthy instances |
demo-lt |
Launch template defining how each instance boots (AMI, SG, User Data) |
demo-alb |
Internet-facing load balancer in public subnets |
demo-asg |
Auto Scaling Group maintaining 2–4 instances in private subnets |
🧹 Cleanup
To avoid ongoing charges:
- Delete the Auto Scaling Group (this terminates its instances too)
- Delete the Load Balancer
- Delete the Target Group
- Delete the Launch Template
- Delete the two security groups (
alb-sg,instance-sg)
🏁 Wrap-Up
You've now built a genuinely production-style pattern: a public-facing load balancer distributing traffic across multiple private, auto-healing instances — with the instances never directly exposed to the internet. This "one door in" architecture is the backbone of most real-world AWS web applications.
Full project files are available on GitHub.



Top comments (0)