DEV Community

Ved Dandotia
Ved Dandotia

Posted on

Building a Highly Available Web Tier on AWS — Application Load Balancer + Auto Scaling Group

⚖️ 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)
Enter fullscreen mode Exit fullscreen mode
  • 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:

  1. A user hits the ALB's DNS name
  2. The ALB (in the public subnet, reachable via the Internet Gateway) receives the request
  3. The ALB forwards it internally — over the VPC's private network — to a healthy instance in the private subnet, via the Target Group
  4. 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)
Enter fullscreen mode Exit fullscreen mode

🧩 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/0 traffic 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

  1. EC2 Console → Security Groups → Create Security Group
  2. Name: alb-sg, Description: "ALB security group", VPC: your demo VPC
  3. Inbound rules: Add rule → Type: HTTP, Port 80, Source: Anywhere-IPv4 (0.0.0.0/0)
  4. Outbound rules: leave default (all traffic allowed)
  5. Create

B) Instance Security Group

  1. Create another security group
  2. Name: instance-sg, Description: "EC2 instance SG", same VPC
  3. 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)
  4. 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

  1. EC2 Console → Target Groups → Create Target Group
  2. Target type: Instances
  3. Name: demo-tg
  4. Protocol: HTTP, Port: 80
  5. VPC: your demo VPC
  6. Health checks:
    • Protocol: HTTP, Path: /
    • Advanced settings: Healthy threshold 2, Unhealthy threshold 2, Timeout 5, Interval 10, Success codes 200
  7. Next → skip "Register targets" (the ASG will handle this automatically) → Create target group

Step 3: Create the Launch Template

  1. EC2 Console → Launch Templates → Create Launch Template
  2. Name: demo-lt
  3. AMI: Amazon Linux 2023
  4. Instance type: t3.micro
  5. Key pair: optional — only needed if you want SSH access for debugging
  6. Network settings: leave subnet blank (the ASG assigns subnets); Security groups: select instance-sg
  7. 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 &
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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

  1. EC2 Console → Load Balancers → Create Load Balancer → Application Load Balancer
  2. Name: demo-alb
  3. Scheme: Internet-facing
  4. IP address type: IPv4
  5. Network mapping: select your VPC → check both public subnets (one per AZ — required, since the ALB needs subnets in 2+ Availability Zones)
  6. Security groups: select alb-sg (remove any default SG that gets auto-added)
  7. Listeners and routing: Listener HTTP:80 → Default action: Forward to → select demo-tg
  8. Create

Wait a minute or two for the state to become Active.


Step 5: Create the Auto Scaling Group

  1. EC2 Console → Auto Scaling Groups → Create Auto Scaling Group
  2. Name: demo-asg
  3. Launch template: select demo-lt → Next
  4. VPC: your demo VPC → Availability Zones and subnets: select both private subnets (one per AZ)
  5. Next → Attach to an existing load balancer → Choose from your load balancer target groups → select demo-tg
  6. 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
  7. Health check grace period: 60 seconds (gives the instance time to boot before checks start counting against it)
  8. Next → Group size:
    • Desired capacity: 2
    • Minimum: 2
    • Maximum: 4
  9. (Optional) Add a scaling policy like "Target tracking on CPU 50%," or skip for now
  10. Review → Create Auto Scaling group

Step 6: Verify

  1. Wait ~2–3 minutes for instances to launch and pass health checks
  2. EC2 Console → Target Groups → demo-tg → Targets tab — both instances should show healthy
  3. EC2 Console → Load Balancers → demo-alb — copy the DNS name (something like demo-alb-123456789.us-east-1.elb.amazonaws.com)
  4. 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:

  1. Delete the Auto Scaling Group (this terminates its instances too)
  2. Delete the Load Balancer
  3. Delete the Target Group
  4. Delete the Launch Template
  5. 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)