TOP NEWS Building a Scalable Web Application on AWS with EC2, ALB, and Auto Scaling
DevOps

Building a Scalable Web Application on AWS with EC2, ALB, and Auto Scaling

10 min read 7 views

A single EC2 instance is a single point of failure. If that instance goes down, the application goes down with it, and if traffic spikes, there is no way to absorb the extra load. The standard AWS pattern for fixing both problems is to put an Application Load Balancer (ALB) in front of a fleet of EC2 instances and let an Auto Scaling Group (ASG) manage how many instances exist at any given time. This guide walks through building that architecture from a blank VPC to a working, load-balanced, auto-scaling web application, using Amazon Linux 2023, Apache, and a CPU-based target tracking scaling policy.

What You Will Build

By the end of this walkthrough, you will have a custom VPC with two public subnets in different Availability Zones, an internet-facing Application Load Balancer, a Launch Template built from a custom AMI, and an Auto Scaling Group that keeps between 2 and 4 Apache web servers running behind the load balancer. A target tracking scaling policy will add or remove instances automatically based on average CPU utilization.

Internet
   |
   v
Application Load Balancer (ALB)
   |
   +-------------------+
   |                    |
   v                    v
EC2 Instance #1     EC2 Instance #2
   Apache               Apache
   |                    |
   +---------+----------+
             |
        Target Group
             |
       Auto Scaling Group
             |
        Launch Template
             |
        CloudWatch
       Scaling Metrics
Architecture diagram showing internet traffic flowing through an Application Load Balancer to two EC2 instances registered in a Target Group, managed by an Auto Scaling Group and Launch Template, with CloudWatch feeding scaling metrics
High-level architecture: the ALB is the only thing clients talk to; the ASG manages instance count behind it.

Prerequisites

  • An AWS account with permissions to create VPC, EC2, ELB, and Auto Scaling resources
  • Basic familiarity with the AWS Management Console
  • An SSH key pair for connecting to EC2 instances
  • No prior networking or load balancing experience required — each step is explained as it happens

Lab Configuration Reference

These are the exact values used throughout this build. Keeping them consistent matters because the ALB, Auto Scaling Group, and security groups all reference each other by name.

Advertisement
ParameterValue
AWS Regionap-south-1 (Mumbai)
VPC CIDR10.0.0.0/16
Public Subnet 110.0.1.0/24, ap-south-1a
Public Subnet 210.0.2.0/24, ap-south-1b
EC2 instance typet3.micro
Operating systemAmazon Linux 2023
Web serverApache (httpd)
ALB listenerHTTP : 80
ASG minimum / desired / maximum2 / 2 / 4
Scaling policyTarget tracking, average CPU utilization, target 50%

Step 1: Create the VPC and Networking Layer

Every resource in this build lives inside a dedicated VPC rather than the account’s default network. This keeps the lab isolated and makes the routing explicit.

  1. In the AWS Console, go to VPC → Your VPCs → Create VPC and choose VPC only.
  2. Name it DevOps-Web-VPC and set the IPv4 CIDR to 10.0.0.0/16.
  3. Create the VPC.

An internet-facing VPC needs an Internet Gateway attached to it, otherwise nothing inside the VPC can reach or be reached from the public internet.

  1. Go to VPC → Internet Gateways → Create internet gateway and name it DevOps-Web-IGW.
  2. After creation, select Actions → Attach to VPC and attach it to DevOps-Web-VPC.

The Auto Scaling Group needs at least two subnets in two Availability Zones so instances can survive the loss of a single AZ. Create two public subnets:

  • Web-Public-Subnet-110.0.1.0/24 in ap-south-1a
  • Web-Public-Subnet-210.0.2.0/24 in ap-south-1b

A subnet is only “public” if its route table sends internet-bound traffic to the Internet Gateway. Create a route table, add a 0.0.0.0/0 route pointing at DevOps-Web-IGW, and associate both subnets with it.

Network diagram showing the DevOps-Web-VPC with CIDR 10.0.0.0/16, two public subnets in ap-south-1a and ap-south-1b each connected through a public route table to an Internet Gateway
The VPC’s two-AZ public subnet layout, both routed to the Internet Gateway.

Step 2: Configure Security Groups

Two security groups keep the traffic boundaries explicit: one for the load balancer and one for the EC2 instances.

  • Web-ALB-SG — allows inbound HTTP (port 80) from 0.0.0.0/0. Add HTTPS (443) if the lab is extended with TLS later.
  • Web-EC2-SG — allows inbound SSH (port 22) from your own IP for administration, and inbound HTTP (port 80). For a tighter design, set the HTTP rule’s source to Web-ALB-SG instead of 0.0.0.0/0, so instances only accept web traffic that has already passed through the load balancer.

Step 3: Launch and Configure the Base EC2 Instance

Before anything can be automated, one instance has to be configured by hand. That instance becomes the template for every server the Auto Scaling Group launches later.

  1. Go to EC2 → Instances → Launch Instance and name it WebServer-Base.
  2. Choose the Amazon Linux 2023 AMI and a t3.micro instance type.
  3. Select or create an SSH key pair.
  4. Set the network to DevOps-Web-VPC, subnet to Web-Public-Subnet-1, and enable auto-assign public IP.
  5. Attach the Web-EC2-SG security group and launch the instance.

Connect to the instance using EC2 Instance Connect and install Apache:

sudo dnf update -y
sudo dnf install httpd -y
sudo systemctl start httpd
sudo systemctl enable httpd
sudo systemctl status httpd

systemctl enable matters as much as start here — it makes sure Apache comes back up automatically if the instance reboots, which is exactly what happens every time the Auto Scaling Group launches a fresh copy of this server later.

Replace the default Apache page with a simple static page so it is obvious, later, which instance is serving a given request:

cd /var/www/html
sudo rm -f index.html
sudo nano index.html
<!DOCTYPE html>
<html>
<head>
  <title>AWS Auto Scaling Demo</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; padding-top: 100px; background: #f4f7fb; }
    .container { background: white; padding: 50px; margin: auto; width: 60%; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); }
    h1 { color: #232f3e; }
    p { font-size: 20px; }
  </style>
</head>
<body>
<div class="container">
  <h1>AWS Auto Scaling Web Application</h1>
  <p>Application Load Balancer is working!</p>
  <p>EC2 Instance is serving this request.</p>
  <p>DevOps AWS Lab</p>
</div>
</body>
</html>

Test it locally with curl http://localhost, then confirm it in a browser using the instance’s public IPv4 address.

Step 4: Create an AMI and Launch Template

Auto Scaling doesn’t configure servers — it launches copies of an image. Everything installed and configured on WebServer-Base needs to be captured into an Amazon Machine Image (AMI) so new instances come up already running Apache with the site deployed.

  1. Select WebServer-Base and choose Actions → Image and templates → Create image.
  2. Name it DevOps-WebServer-AMI and create it.
  3. Wait for the AMI status to become Available before continuing.

The Launch Template is the configuration Auto Scaling actually reads when it decides to add a new instance — AMI, instance type, key pair, and security group all live here in one reusable definition.

  1. Go to EC2 → Launch Templates → Create launch template.
  2. Name it DevOps-Web-LaunchTemplate.
  3. Select the DevOps-WebServer-AMI, instance type t3.micro, the SSH key pair, and the Web-EC2-SG security group.
  4. Create the template.

Step 5: Create the Target Group and Application Load Balancer

A Target Group is the object the load balancer actually routes to — it holds the list of instances and the health check that decides whether each one is allowed to receive traffic.

  1. Go to EC2 → Target Groups → Create target group, target type Instances.
  2. Name it DevOps-Web-TG, protocol HTTP, port 80, in DevOps-Web-VPC.
  3. Set the health check protocol to HTTP and the health check path to /.
  4. Create the target group without manually registering instances — the Auto Scaling Group will register and deregister them automatically as it scales.
  1. Go to EC2 → Load Balancers → Create Load Balancer and choose Application Load Balancer.
  2. Name it DevOps-Web-ALB, scheme Internet-facing, IP address type IPv4.
  3. Add an HTTP : 80 listener, select DevOps-Web-VPC, and select both Web-Public-Subnet-1 and Web-Public-Subnet-2 — the ALB needs at least two AZs to be created.
  4. Attach the Web-ALB-SG security group.
  5. Set the HTTP listener’s default action to forward to DevOps-Web-TG, then create the load balancer.

Step 6: Create the Auto Scaling Group

This is the piece that ties everything together: it uses the Launch Template to know what an instance should look like, the Target Group to know where to register new instances, and a scaling policy to know when to add or remove them.

  1. Go to EC2 → Auto Scaling Groups → Create Auto Scaling group, name it DevOps-Web-ASG.
  2. Select DevOps-Web-LaunchTemplate, choose DevOps-Web-VPC, and select both public subnets.
  3. Attach the existing DevOps-Web-TG target group and enable ELB health checks — this makes the ASG treat a failed ALB health check the same as an unhealthy instance and replace it.
  4. Set desired capacity to 2, minimum to 2, and maximum to 4.
  5. Add a target tracking scaling policy on average CPU utilization with a target value of 50%, then create the group.

Step 7: Test the Application Through the Load Balancer

Go to EC2 → Load Balancers → DevOps-Web-ALB and copy its DNS name. Opening that address in a browser should load the same static page, but now it is being served through the load balancer rather than any single instance’s public IP.

Refreshing the page repeatedly will occasionally show a different instance responding, since the ALB distributes requests across every healthy target in the group. In this lab both instances serve an identical page, but in a real deployment this is the point where you would notice, for example, one instance running an older application version.

How Auto Scaling Responds to Load

With minimum 2, desired 2, and maximum 4, the group is free to move capacity anywhere in that range as CloudWatch metrics change. The two directions work as mirror images of each other.

Scale-Out

Traffic increases
  -> Application workload increases
  -> EC2 CPU utilization increases
  -> CloudWatch observes the metric
  -> Scaling policy evaluates the target
  -> Auto Scaling Group launches another EC2 instance
  -> New instance passes health checks
  -> Instance is registered with the Target Group
  -> ALB begins sending traffic to the healthy instance

Scale-In

Traffic decreases
  -> Application workload decreases
  -> CPU utilization decreases
  -> CloudWatch observes the metric
  -> Scaling policy evaluates the target
  -> Auto Scaling Group can terminate an unnecessary instance
  -> Target Group removes the instance
  -> ALB sends traffic to remaining healthy instances
Diagram showing CloudWatch monitoring EC2 CPU utilization and feeding a target tracking scaling policy that adds instances to the Auto Scaling Group above 50 percent CPU and removes them below it
The feedback loop that drives scale-out and scale-in decisions.

Scaling is never instantaneous. AWS has to evaluate the CloudWatch metric against the policy, launch or terminate the instance, and — on scale-out — wait for the new instance to pass its health checks before the ALB will send it traffic. Expect this to take a few minutes rather than seconds.

Generating a Scale-Out Event for Testing

To see scaling happen instead of just reading about it, generate sustained CPU load on one of the running instances with stress-ng:

sudo dnf install stress-ng -y
stress-ng --cpu 2 --timeout 300s

Watch the Auto Scaling Group’s Activity tab and the target group’s CloudWatch metrics — healthy host count, target response time, and request count will all move once a new instance registers.

Common Mistakes

  • Forgetting to enable “auto-enable” on systemctl enable httpd. If Apache isn’t set to start on boot, every new instance the Auto Scaling Group launches will fail its target group health check even though the instance itself is running fine.
  • Building the Launch Template from a live instance instead of a clean AMI. Skipping the AMI step and pointing the Launch Template at the running WebServer-Base instance directly isn’t possible — the AMI capture is what makes the configuration reproducible for new instances.
  • Registering instances into the target group by hand. If the Auto Scaling Group doesn’t own the registration, it can’t cleanly deregister instances during scale-in, which leaves stale or terminated instances listed as targets.
  • Leaving the EC2 security group open to 0.0.0.0/0 on port 80. Once the ALB is in place, application servers should only need to accept HTTP traffic that has already passed through Web-ALB-SG, not directly from the internet.
  • Single-subnet deployments. An ALB requires subnets in at least two Availability Zones; placing both EC2 instances in one AZ also defeats the availability benefit Auto Scaling is meant to provide.

Production Considerations

The design in this walkthrough puts EC2 instances directly in public subnets, which is fine for a lab but not ideal for production. A stronger design keeps the internet-facing ALB in public subnets while moving the application instances into private subnets with no direct route to the internet. Outbound internet access for patching still works through a NAT Gateway, but nothing on the internet can reach the application servers directly — only the ALB security group is allowed to send them traffic.

Internet
   |
   v
Internet-facing ALB
   |
   +--------------------+
   |                     |
Private Subnet       Private Subnet
   |                     |
  EC2                   EC2
   |                     |
   +---------+-----------+
             |
        NAT Gateway
Production architecture diagram showing an internet-facing Application Load Balancer in public subnets forwarding traffic to EC2 instances in private subnets, with outbound access through a NAT Gateway
Production variant: application instances moved into private subnets, reachable only through the ALB.

Beyond the network layout, a production deployment would typically add HTTPS termination on the ALB, CloudWatch alarms for the target group’s unhealthy host count, and a CI/CD pipeline that builds a new AMI and rolls it out through an instance refresh rather than editing servers by hand.

Mental Model

The whole architecture reduces to one chain: the Launch Template defines what an instance is, the Auto Scaling Group decides how many should exist, the Target Group tracks which ones are currently healthy, and the Application Load Balancer is the only thing a client ever actually talks to. Nothing about the client-facing address changes as instances are added, removed, or replaced underneath it.

Conclusion

This build takes a static Apache site from a single EC2 instance to a self-healing, load-balanced, auto-scaling deployment using nothing beyond core AWS building blocks: a custom VPC, an AMI, a Launch Template, a Target Group, an Application Load Balancer, and an Auto Scaling Group with a CPU-based target tracking policy. The same pattern — bake configuration into an AMI, template it, and let Auto Scaling manage capacity behind a load balancer — scales directly to real applications, with the private-subnet variant covered above being the natural next step toward a production-ready setup.

Frequently Asked Questions

Why does the Auto Scaling Group need two subnets in different Availability Zones?

An Application Load Balancer requires subnets in at least two AZs to be created at all, and spreading EC2 instances across AZs means the loss of one data center doesn’t take the whole application down.

Why use ELB health checks instead of the default EC2 health checks on the Auto Scaling Group?

EC2 health checks only detect whether the instance itself is running. ELB health checks call the actual health check path on the application, so an instance where the OS is fine but Apache has crashed still gets marked unhealthy and replaced.

What happens if desired capacity is set above the maximum?

AWS will not accept it — desired capacity must always fall between the configured minimum and maximum, and the scaling policy can only move desired capacity within that same range.

Why is a Launch Template needed if the AMI already has everything installed?

The AMI only captures the disk image. The Launch Template is what tells the Auto Scaling Group which instance type, key pair, and security group to use when it launches a new instance from that image.

Share:

Author at GetCloud.in – Docker, Kubernetes, Linux & Cloud Tutorials

Previous
What Happens When a Kubernetes Pod Dies? A Step-by-Step Failure and Recovery Walkthrough