Build a Resilient Auto-Scaling Web App
Deploy a self-healing web app with EC2 Auto Scaling and an Application Load Balancer.
Introduction
30 Second Summary
Every time you visit a website, you trust that it will load. Behind the scenes, cloud engineers build systems that keep sites running even when servers crash or traffic surges beyond what a single machine can handle.
In this project, you will build a self-healing, auto-scaling web application on AWS that survives instance failures with zero downtime and automatically adds capacity when demand spikes.
What You'll Build
You will deliberately terminate a running server, watch a replacement appear automatically, then spike CPU load and watch new instances spin up to absorb the traffic. All while your site stays live at the same URL.
By the end of this project, you'll have:
- A self-healing web server fleet where you can terminate any instance and a new one appears within minutes, keeping your site online.
- A stable public URL powered by an Application Load Balancer that distributes traffic across healthy instances in multiple Availability Zones.
- A CloudWatch alarm wired to a scaling policy that detects CPU spikes and automatically launches new instances to handle the load.
- Secret Mission: Wire up SNS email notifications so you get alerted every time your infrastructure scales.
Are there any prerequisites?
You need an AWS account (free tier or paid) and a Windows PC with a web browser. Step 1 walks you through setting up the AWS CLI, so no prior CLI experience is needed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to build a self-healing, auto-scaling web application on AWS that survives instance failures and grows under load. This project covers the exact services and patterns tested in SAA-C03 Domain 2 (Resilient Architectures, 26% of the exam) and Domain 3 (High-Performing Architectures, 24%).
Set Up Your AWS Account and CLI
Every AWS resource you build in this project lives inside your personal AWS account. The AWS CLI connects your local terminal to that account so you can create, inspect, and tear down resources without clicking through the console every time.
In this step, you will create an AWS account (or sign in to your existing one), set up an IAM user with the right permissions, and install the AWS CLI on your Windows machine.
In this step, get ready to:
- Create an AWS account and an IAM user with AdministratorAccess.
- Install AWS CLI v2 on Windows.
- Configure the CLI with your credentials and verify the connection.
Create an AWS account and IAM user
Your AWS account is the billing container for everything you build. The IAM user gives you a separate identity with its own credentials, keeping your root account locked away safely.
- Go to https://aws.amazon.com and either sign in to your existing account or click Create an AWS Account and follow the prompts.
- Once signed in to the AWS Management Console, type IAM in the top search bar and select IAM from the results.
- In the left navigation pane, click Users.
- Click Create user.
- For User name, enter a name like admin-cli.
- Click Next.
- On the Set permissions page, select Attach policies directly.
- In the search box, type AdministratorAccess and check the box next to the AdministratorAccess policy.
- Click Next, then click Create user.
Your user now appears in the Users list. Next, you will generate an access key pair so the CLI can authenticate as this user.
- Click the user name you just created to open its details page.
- Select the Security credentials tab.
- Under Access keys, click Create access key.
- For the use case, select Command Line Interface (CLI).
- Check the confirmation checkbox at the bottom, then click Next.
- Click Create access key.
Save your keys now
You will see your Access Key ID and Secret Access Key on this page. The Secret Access Key is shown only once. Copy both values and save them somewhere safe (a password manager or a local text file). You will need them in a moment to configure the CLI.
- Click Done.
Why an IAM user instead of root?
Your root account has unrestricted access to everything, including billing. An IAM user with AdministratorAccess can do everything you need for this project while keeping root credentials locked away. If the IAM key is ever compromised, you can revoke it without losing access to your entire account.
Install AWS CLI v2 on Windows
The AWS CLI lets you manage AWS resources from your terminal. You will use it throughout this project to inspect Auto Scaling Groups, terminate instances, and check CloudWatch alarms.
- Download the AWS CLI v2 installer for Windows by visiting this link in your browser:
- https://awscli.amazonaws.com/AWSCLIV2.msi.
- Run the downloaded AWSCLIV2.msi file and follow the on-screen prompts to complete the installation.
- Close any open Command Prompt or PowerShell windows after the install finishes.
What if I don't have admin rights?
The all-users installer requires admin rights. If you do not have admin access, download the current-user installer instead from https://awscli.amazonaws.com/AWSCLIV2-User.msi. It installs for your account only and does not require elevation.
Now verify the CLI is installed. Open a fresh Command Prompt window.
- Press the Windows key to open your search bar.
- Type cmd and press Enter to open Command Prompt.
- Check your CLI version by running:
aws --version
✔️ I see aws-cli/2.x.x
You should see output like aws-cli/2.27.41 Python/3.11.6 Windows/10 exe/AMD64 prompt/off. The exact version number may differ. As long as it starts with aws-cli/2, you are good to go.
ⓧ I see aws-cli/1.x.x
You have an older version installed. Uninstall it first.
- Press the Windows key, type Add or remove programs, and press Enter.
- Find AWS Command Line Interface in the list and click Uninstall.
- After uninstalling, download and run the v2 installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
- Close and reopen Command Prompt, then run aws --version again.
ⓧ Command not found
Windows cannot find the aws command. Try these fixes.
- Close your Command Prompt window completely and open a brand new one. The PATH update only takes effect in new windows.
- If it still does not work, re-run the MSI installer to repair the installation.
- Make sure you downloaded AWSCLIV2.msi (the v2 installer), not the older v1 installer.
Configure the CLI with your credentials
The CLI needs your access keys, a default region, and an output format. The aws configure command walks you through setting all four values.
- In your Command Prompt, run:
aws configure
What will this command ask?
The command prompts you for four values one at a time. Enter the following:
- AWS Access Key ID: paste the Access Key ID you saved earlier.
- AWS Secret Access Key: paste the Secret Access Key you saved earlier.
- Default region name: type us-east-1.
- Default output format: type json.
- Enter each value when prompted and press Enter after each one.
Now verify the CLI can reach your AWS account. Before you run the next command, think about what you expect to see. If the credentials are correct, the CLI should return information about your IAM identity.
- Verify your connection by running:
aws sts get-caller-identity
You should see JSON output containing your Account ID, UserId, and an ARN that includes the IAM user name you created. This proves your CLI is authenticated and talking to the right AWS account.
Seeing an error?
- If you see a credential error, double-check that you pasted the Access Key ID and Secret Access Key correctly. Run aws configure again to re-enter them.
- If you see a network timeout, check your internet connection and try again.
- If the region is wrong in the output, run aws configure and set the default region to us-east-1.
Still stuck? Help me troubleshoot my aws sts get-caller-identity error
Your AWS account is live and your CLI is connected. Next up, you will deploy your first web server inside an Auto Scaling Group.
Deploy a Single-Instance Auto Scaling Group
In the previous step, you set up your AWS account and CLI access. Now you need something to actually protect.
Before you can build a resilient architecture, you need a running web server. This step creates the simplest possible auto-scaling setup: one instance serving a web page. It works, but it has a hidden vulnerability you will discover in the next step.
In this step, get ready to:
- Create a Launch Template that defines your web server's configuration.
- Create an Auto Scaling Group that launches one instance from that template.
- Verify your web server is running and serving a page.
Create a Launch Template
A Launch Template is a saved configuration that tells AWS how to launch an EC2 instance. It stores the operating system image, instance size, security rules, and any startup commands your server needs.
- Open the EC2 console at https://console.aws.amazon.com/ec2/.
- In the left navigation pane, click Launch Templates.
- Click Create launch template.
- In the Launch template name field, enter resilient-web-template.
- Under Application and OS Images (Amazon Machine Image), search for and select Amazon Linux 2 AMI (free tier eligible).
- Under Instance type, select t3.micro.
Why Amazon Linux 2 and t3.micro?
Amazon Linux 2 is AWS's own Linux distribution, optimized for EC2. It comes with yum pre-installed, which your startup script uses to install the web server.
The t3.micro instance type is free tier eligible for new AWS accounts (created after July 15, 2025). It provides enough compute power for a test web server while keeping costs near zero.
- Under Network settings, click Create security group.
- Enter web-sg in the Security group name field.
- Add an inbound rule with Type set to HTTP, Port 80, and Source set to Anywhere (0.0.0.0/0).
What is a security group?
A security group acts as a virtual firewall for your instance. The HTTP rule you just added allows anyone on the internet to access your web server on port 80. Without this rule, browsers would not be able to reach your page.
Now you need to add a startup script that installs and configures your web server automatically when the instance launches.
- Scroll down and expand the Advanced details section.
- Scroll to the User data field at the bottom.
- Paste the following script into the User data field:
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
echo "<h1>Hello from $INSTANCE_ID</h1>" > /var/www/html/index.html
What does this script do?
This is a User Data script. AWS runs it automatically when an instance first boots.
- It installs Apache (httpd) and starts it as a web server.
- It fetches the instance's own ID using IMDSv2 (Instance Metadata Service v2), a secure token-based method to query instance metadata.
- It writes a simple HTML page showing "Hello from" followed by the instance ID, so you can tell which instance served the response.
- Click Create launch template.
- You should see a success banner confirming your template resilient-web-template was created.
Not seeing the success message?
- Scroll back up and check for a red error banner. A common issue is leaving the Security group name field empty or using a name that already exists in your account.
- Verify you selected an AMI under Application and OS Images. If it says "Don't include in launch template", your ASG instances will fail to launch.
Help me troubleshoot my launch template creation error.
Create an Auto Scaling Group
An Auto Scaling Group (ASG) uses your launch template to launch and manage instances. It monitors their health and automatically replaces any that fail.
- In the left navigation pane of the EC2 console, click Auto Scaling Groups.
- Click Create Auto Scaling group.
- In the Auto Scaling group name field, enter resilient-web-asg.
- Under Launch template, select resilient-web-template.
- Click Next.
- Under Network, keep the VPC set to your default VPC.
- Under Availability Zones and subnets, select only ONE subnet (any single Availability Zone). This is intentional for now.
- Click Skip to review.
Why only one Availability Zone?
You are deliberately creating a fragile setup. Picking a single subnet means all your instances live in one data center. This lets you experience the exact failure mode that multi-AZ architectures solve.
- On the Review page, confirm the Group size shows Desired capacity: 1, Minimum: 1, Maximum: 4.
- Confirm that no load balancer is attached.
- Click Create Auto Scaling group.
Capacity defaults
If the desired/min/max values are not already set to 1/1/4, edit them on the Review page before creating. Desired 1 means one instance launches immediately. Maximum 4 gives room to scale later.
Verify your web server is running
Your ASG should now be launching one instance. Give it about 1-2 minutes to boot and run the User Data script.
- Confirm your ASG is running by executing this command in your terminal:
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names resilient-web-asg
You should see JSON output showing your group with one instance listed under Instances with a LifecycleState of InService.
- In the EC2 console, click Instances in the left navigation pane.
- Find the running instance launched by your ASG (it will have a state of Running).
- Click on the instance to see its details.
- Copy the Public IPv4 address from the instance details panel.
Before you paste this IP into your browser, what do you think you will see?
- Paste the public IP address into your browser's address bar and press Enter.
You should see a page displaying "Hello from i-xxxxxxxxx" where the x's are your instance's unique ID. This confirms your Launch Template, User Data script, and security group are all working together.
Seeing a timeout or connection refused?
- Wait 2 minutes. The instance needs time to boot, run yum update, and install Apache.
- Check that your security group web-sg has an inbound rule for HTTP (port 80) from 0.0.0.0/0. You can verify this under Security Groups in the EC2 console.
- Make sure you are using http:// (not https://) in your browser. Your server only listens on port 80.
Why can't I reach my instance's web page?
Your web server is live and serving traffic. But there is a problem lurking beneath the surface. What happens if this single instance dies? In the next step, you will break it on purpose and find out.
Break It and Expose the Single Point of Failure
Your web server is live and serving pages from a single instance. Everything works perfectly until something goes wrong.
Real-world instances fail all the time. Hardware faults, kernel panics, entire Availability Zone outages. Your Auto Scaling Group will replace a failed instance, but what happens to your users in the meantime? In this step, you will deliberately break what you built and discover the answer firsthand.
In this step, get ready to:
- Terminate your running instance to simulate a failure.
- Experience the downtime your users would face.
- Confirm the ASG self-heals, then identify the two problems this architecture leaves unsolved.
Terminate your running instance
Before you can appreciate what a load balancer does, you need to feel the pain of not having one. You are going to kill your only instance and observe what happens.
- Return to the EC2 console and go to the Instances page.
- Note the Public IPv4 address of your running instance. Write it down or keep the tab open.
- Get your instance ID by running this command in your terminal:
aws autoscaling describe-auto-scaling-instances --query "AutoScalingInstances[?AutoScalingGroupName=='resilient-web-asg'].InstanceId" --output text
What does this command do?
This queries your Auto Scaling Group and returns just the instance ID of the instance it manages. The --query flag filters the JSON response so you only see the ID. The --output text strips the quotes so you can copy it cleanly.
You should see an instance ID like i-0abc123def456789 in your terminal output.
Before you terminate, think about this: what do you expect will happen to your website when you destroy the only instance serving it?
- Terminate the instance by running this command:
aws ec2 terminate-instances --instance-ids [[INSTANCE_ID="your instance ID from the previous command"]]
You should see output confirming the instance state is changing from running to shutting-down.
Seeing an 'InvalidInstanceID' error?
Make sure you copied the full instance ID from the previous command, including the i- prefix. The ID should look like i-0abc123def456789.
Help me troubleshoot why my terminate command failed
Watch the site go down and recover
- Immediately switch to your browser tab with the old IP address.
- Refresh the page.
The site is down. Your browser shows an error because the instance no longer exists. This is the downtime your users would experience.
- Wait 1-2 minutes for the ASG to detect the failure and launch a replacement.
- Check on the recovery by running this command:
aws autoscaling describe-auto-scaling-activities --auto-scaling-group-names resilient-web-asg
What should I see?
Look for two activities in the output. One with a Description mentioning termination, and another describing a new instance launch. The StatusCode field shows whether each activity is still InProgress or Successful.
If you only see the termination activity, wait another minute and run the command again. The ASG needs time to detect the failure and spin up a replacement.
Only seeing one activity after several minutes?
The ASG health check interval can take up to a few minutes. Run the command again after waiting. If you still see no launch activity, check that your ASG still has DesiredCapacity set to 1 in the console.
Help me figure out why self-healing isn't working
Confirm the replacement and identify the problems
Once you see a successful launch activity, you need to find the new instance and prove the site is back at a different address.
- Return to the EC2 console Instances page.
- Find the new running instance (the old one shows as Terminated).
- Copy the new instance's Public IPv4 address. Notice it is different from the one you wrote down earlier.
- Paste the new IP into your browser address bar and press Enter.
You should see the familiar "Hello from i-xxxxxxxxx" page, but with a completely different instance ID. The site recovered, but at a new address.
What did you just prove?
Your Auto Scaling Group is self-healing. It detected the terminated instance and launched a replacement automatically. That is powerful.
But you also exposed two critical problems. First, your users experienced real downtime while the replacement launched. Second, the new instance has a different public IP, so anyone who bookmarked or hardcoded the old address is now pointing at nothing.
- Run the activities command one final time to confirm both events completed:
aws autoscaling describe-auto-scaling-activities --auto-scaling-group-names resilient-web-asg
You should see both a termination activity and a launch activity with StatusCode showing Successful.
You now have hard evidence that a single-instance setup is fragile. The ASG heals itself, but your users pay the price in downtime and broken links.
Next up, you will solve both problems with an Application Load Balancer that gives clients one stable address and routes around failures instantly.
Add an Application Load Balancer for Stability and Multi-AZ
In Step 3, you proved that your ASG self-heals. But you also exposed two painful problems: the site goes down during replacement, and the IP address changes every time.
An Application Load Balancer solves both. It gives clients a single stable DNS name and distributes traffic across instances in multiple Availability Zones. Losing one instance causes zero downtime. This is the core pattern behind most SAA-C03 resilience questions.
In this step, get ready to:
- Create a Target Group to register your instances.
- Create an Application Load Balancer spanning multiple Availability Zones.
- Update your Auto Scaling Group to use the ALB and prove zero-downtime resilience.
Create a target group
A Target Group tells the ALB where to send incoming traffic. You create it first, then point the ALB at it.
- In the EC2 console, scroll down the left sidebar to the Load Balancing section.
- Click Target Groups.
- Click Create target group.
- For target type, select Instances.
- Set the target group name to resilient-web-tg.
- Leave Protocol as HTTP and Port as 80.
- Under Health checks, confirm the health check path is set to /.
- Click Next.
Why not register targets manually?
On the Register targets page, do NOT select any instances. Your Auto Scaling Group will automatically register its instances with this target group when you attach it in a later substep. Manually registering would create duplicate registrations.
- Skip the target registration page (do not select any instances) and click Create target group.
You should see resilient-web-tg listed in your target groups with 0 registered targets.
Don't see the target group?
Make sure you are in the same region where you created your ASG. Check the region selector in the top-right corner of the console.
If you see a name-conflict error, a target group with that name already exists. Delete the old one first or choose a slightly different name.
Why isn't my target group appearing in the console?
Create an Application Load Balancer
The ALB is the single stable endpoint your clients will use. It distributes traffic across all healthy instances registered in the target group.
- In the left sidebar under Load Balancing, click Load Balancers.
- Click Create load balancer.
- Under Application Load Balancer, click Create.
- Set the name to resilient-web-alb.
- For Scheme, select Internet-facing.
- Under Network mapping, select your default VPC.
- Select at least 2 Availability Zones by checking subnets in 2 or 3 different AZs.
Why at least 2 Availability Zones?
AWS requires you to enable zones for at least two Availability Zones when creating an ALB. This ensures traffic can still reach healthy instances even if an entire data center (AZ) goes offline.
- Under Security groups, remove the default security group and select web-sg (the one you created earlier that allows HTTP port 80 inbound).
- Under Listeners and routing, confirm the listener is set to HTTP on port 80.
- In the Default action dropdown, select your target group resilient-web-tg.
- Scroll to the bottom and click Create load balancer.
You should see a success message. The ALB state starts as provisioning and transitions to active within 2-3 minutes.
- Click View load balancer (or navigate to your load balancer in the list).
- Wait until the State column shows active. Refresh the page if needed.
- Copy the DNS name from the load balancer's Description tab. You will use this URL shortly.
ALB stuck in provisioning?
The most common cause is a missing internet gateway on the VPC. The default VPC already has one, so this should not happen if you selected the default VPC. If you selected a custom VPC, ensure it has an internet gateway attached.
Why is my ALB stuck in provisioning state?
Update your Auto Scaling Group and verify zero downtime
Your ALB and target group are ready, but your ASG does not know about them yet. You need to connect the ASG to the target group, expand it across multiple AZs, and increase capacity to 2 instances.
- In the EC2 console, click Auto Scaling Groups in the left sidebar.
- Select resilient-web-asg and click Edit on the Details tab.
- Under Network, change the subnets to include at least 2 Availability Zones (select the same AZs you chose for the ALB).
- Set Desired capacity to 2.
- Click Update to save these changes.
Now attach the target group and enable ELB health checks.
- With your ASG still selected, go to the Load balancing section and click Edit.
- Under Application, Network or Gateway Load Balancer target groups, select resilient-web-tg.
- Click Update.
- Next, go to the Health checks section and click Edit.
- Turn on Elastic Load Balancing health checks.
- Set the health check grace period to 120 seconds.
- Click Update.
What is the health check grace period?
The grace period is how long Amazon EC2 Auto Scaling waits before checking the health status of a new instance. A 120-second window gives each instance time to boot, run the user data script, and start Apache before the load balancer checks if it is healthy.
Without this grace period, the ASG could terminate instances that are still starting up.
Wait 2-3 minutes for the second instance to launch and pass its health check.
- Navigate to Target Groups in the left sidebar.
- Click resilient-web-tg and open the Targets tab.
- Confirm that both instances show a status of healthy. If they still show initial, wait another minute and refresh.
Both instances are healthy. Time to test load distribution.
- Paste the ALB DNS name you copied earlier into your browser's address bar.
- Refresh the page several times.
You should see the instance ID in the greeting change between refreshes. This proves the ALB is distributing traffic across your instances in different Availability Zones.
Now for the real test. Before you run the next command, do you think the site will stay up or go down when you terminate one of the two instances?
- Get one of your instance IDs by running this command in your terminal:
aws autoscaling describe-auto-scaling-instances --query "AutoScalingInstances[?AutoScalingGroupName=='resilient-web-asg'].InstanceId" --output text
- Pick either instance ID from the output and terminate it:
aws ec2 terminate-instances --instance-ids [[INSTANCE_ID="your instance ID from the previous command"]]
- Immediately switch to your browser and refresh the ALB DNS name.
The site stays up. Zero downtime. The ALB routes all traffic to the remaining healthy instance while the ASG launches a replacement. Compare this to Step 3 where terminating the only instance caused the site to go completely offline.
What just happened?
The ALB detected the terminated instance as unhealthy and stopped routing traffic to it within seconds. All requests went to the surviving instance instead. Meanwhile, the ASG noticed the capacity dropped below 2 and launched a fresh replacement in a different AZ.
This is Multi-AZ resilience: even if an entire Availability Zone goes offline, instances in other AZs keep serving traffic through the same stable DNS name.
Site went down when you terminated?
Make sure you are hitting the ALB DNS name (it looks like resilient-web-alb-123456789.us-east-1.elb.amazonaws.com), not the individual instance IP address.
If the ALB DNS shows an error, check that both instances were healthy in the target group before you terminated one. If only one was registered, the ALB had no fallback.
Why is my ALB returning errors after I terminated an instance?
You now have a production-style resilient architecture. A stable URL that survives instance failures with zero downtime. Next up, you will make it respond to traffic spikes by adding CloudWatch alarms and dynamic scaling policies.
Add CloudWatch Alarms and Dynamic Scaling
Your ALB now gives clients a stable URL that survives instance failures with zero downtime. Traffic flows evenly across healthy instances in multiple Availability Zones. But your capacity is locked at exactly 2 instances.
If a traffic spike overwhelms both instances, the site degrades and users experience slow responses. Dynamic scaling solves this by letting your ASG grow automatically when CPU rises and shrink when load drops.
In this step, get ready to:
- Create a target tracking scaling policy on your Auto Scaling Group.
- Create a CloudWatch alarm to monitor CPU spikes.
- Stress test an instance and watch the ASG scale out automatically.
Create a target tracking scaling policy
A target tracking scaling policy works like a thermostat. You set a target value (like 50% CPU), and AWS automatically adjusts capacity to stay near that target.
- In the AWS Console, navigate to EC2 from the top search bar.
- In the left sidebar, scroll down to Auto Scaling Groups under the Auto Scaling section.
- Click on resilient-web-asg to open your Auto Scaling Group.
- Select the Automatic scaling tab.
- Click Create dynamic scaling policy.
- For Policy type, select Target tracking scaling.
- For Metric type, select Average CPU utilization.
- Set the Target value to 50.
- Leave Instance warmup at 300 seconds.
- Click Create.
You should see the new policy listed under the Automatic scaling tab with the metric type showing Average CPU utilization and target value of 50.
What does target tracking do behind the scenes?
When you create this policy, AWS automatically creates two CloudWatch alarms on your behalf. One alarm triggers scale-out when CPU rises above the target. The other triggers scale-in when CPU drops well below it.
The 300-second warmup tells AWS to wait 5 minutes before including a new instance's metrics in the average. This prevents the ASG from scaling again before new instances have finished booting and stabilised.
Why target tracking instead of step scaling?
AWS recommends target tracking as the simplest and most effective scaling approach for most use cases. Step scaling requires you to manually define thresholds and increments, adding complexity. Target tracking handles all that math for you by just maintaining a target.
Create a CloudWatch alarm for CPU visibility
The target tracking policy creates its own alarms behind the scenes, but those are managed by AWS and hard to inspect. You will create your own alarm so you can clearly see when CPU spikes above 60%.
- In the AWS Console top search bar, type CloudWatch and select the CloudWatch service.
- In the left sidebar, click Alarms, then All alarms.
- Click Create alarm.
- Click Select metric.
- Choose EC2, then By Auto Scaling Group.
- Find resilient-web-asg and check the box next to CPUUtilization.
- Click Select metric.
Now configure the alarm threshold.
- Set Statistic to Average.
- Set Period to 1 minute.
- Under Conditions, select Greater than and enter 60 as the threshold value.
- Click Next.
- On the Configure actions page, select Remove next to the notification to skip SNS for now.
- Click Next.
- For Alarm name, enter ScaleOutAlarm.
- Click Next, then Create alarm.
You should see ScaleOutAlarm appear in the alarms list with a state of OK (because CPU is currently low).
What does this alarm do?
This alarm monitors the average CPU across your ASG every minute. When CPU exceeds 60%, the alarm state changes from OK to ALARM. Right now it has no notification action attached. You will wire up SNS email alerts in the Secret Mission.
Stress test and verify dynamic scaling
Time to prove that scaling actually works. You will connect to one of your instances and generate artificial CPU load to trigger the scaling policy.
First, your security group needs to allow SSH access for EC2 Instance Connect to work.
- Navigate back to EC2 in the AWS Console.
- In the left sidebar, click Security Groups under the Network & Security section.
- Select web-sg.
- Click Edit inbound rules.
- Click Add rule.
- Set Type to SSH, and Source to Anywhere-IPv4 (0.0.0.0/0).
- Click Save rules.
Why add an SSH rule?
EC2 Instance Connect uses SSH (port 22) to establish a browser-based terminal session with your instance. Your web-sg security group only allows HTTP on port 80, so without this rule the connection would be refused.
Now connect to one of your instances.
- In the left sidebar, click Instances.
- Select any running instance that belongs to resilient-web-asg.
- Click Connect at the top of the page.
- On the Connect to instance page, select the EC2 Instance Connect tab.
- Leave the username as ec2-user.
- Click Connect.
A browser-based terminal session opens, connected directly to your instance.
Connection timing out?
Make sure you saved the SSH inbound rule on web-sg in the previous step. Also verify the instance's state is Running and that it has a public IP address assigned.
help me troubleshoot EC2 Instance Connect timing out
Now generate CPU load to trigger scaling. Run this stress command in the Instance Connect terminal:
for i in $(seq 1 4); do yes > /dev/null & done
What does this command do?
This launches 4 background processes that each run yes (which outputs "y" endlessly) and pipes it to /dev/null. Each process pegs one CPU thread at 100%. On a t3.micro with 2 vCPUs, this will quickly drive CPU utilization far above the 50% target and the 60% alarm threshold.
Before you check the results, predict: do you think CloudWatch will show the CPU spike within 30 seconds, or will it take longer?
- Wait 2-3 minutes for CloudWatch to register the spike.
- While waiting, switch back to CloudWatch in the AWS Console.
- Click All alarms and watch ScaleOutAlarm change state from OK to In alarm.
CloudWatch evaluates metrics on a 1-minute period, so it takes at least a couple of minutes to register the spike and change alarm state.
Now verify that the ASG has responded by scaling out. Open a terminal on your local machine and run this command:
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names resilient-web-asg --query "AutoScalingGroups[0].DesiredCapacity"
You should see the number has increased beyond 2. Your ASG detected the CPU spike and launched additional instances to bring the average back toward the 50% target.
Still seeing 2?
The target tracking policy can take 3-5 minutes to react after the alarm fires. Wait another minute or two and run the command again. Also check that the stress command is still running by switching back to your Instance Connect tab.
help me figure out why my ASG is not scaling out
You can also confirm the alarm state from the CLI:
aws cloudwatch describe-alarms --alarm-names ScaleOutAlarm --query "MetricAlarms[0].StateValue"
You should see "ALARM" confirming the threshold was breached.
Now stop the stress test. Switch back to your EC2 Instance Connect browser tab and run:
killall yes
This stops all the background yes processes. Over the next 5-15 minutes, CPU drops back down and the ASG will gradually scale back in (remove the extra instances). You do not need to wait for this to complete.
Why does scale-in take longer?
AWS intentionally makes scale-in slower than scale-out. Scaling out quickly protects your users from overload. Scaling in slowly prevents flapping (repeatedly adding and removing instances if load fluctuates).
Secret mission
Wire Up SNS Scaling Notifications
In production, operators need to know when infrastructure scales. Can you wire up email notifications so you get alerted every time your ASG launches or terminates an instance?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your ALB and EC2 instances accrue charges every hour they run, so delete everything if you are done with this project.
Cost warning
Your Application Load Balancer charges approximately $0.0225 per hour (about $0.55/day) just for existing, plus per-instance EC2 costs. If you leave these resources running overnight or over a weekend, you could accumulate a few dollars in charges.
Delete everything when you are finished to avoid unexpected bills.
Resources you used:
- Auto Scaling Group: resilient-web-asg (billed per instance-hour for EC2).
- Application Load Balancer: resilient-web-alb (billed per hour plus LCU charges).
- Target Group: resilient-web-tg (no standalone charge, but must be deleted with the ALB).
- Launch Template: resilient-web-template (no charge, but clutters your account).
- Security Group: web-sg (no charge, but should be cleaned up).
- CloudWatch Alarm: ScaleOutAlarm (free for up to 10 alarms).
- SNS Topic: scaling-alerts (free for up to 1,000 email notifications/month).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your resilient architecture.
- Your ALB and ASG will continue running and accruing charges (approximately $0.55/day for the ALB alone, plus EC2 instance costs).
- The CloudWatch alarm and SNS topic remain active but cost nothing at low usage.
- You can continue to access your application at the ALB DNS name.
Note that the AWS CLI remains installed on your machine. This is useful for future AWS projects.
Pause - I'll come back to this later
Reduce costs by setting your ASG desired capacity to 0. This terminates all instances but keeps your configuration intact so you can scale back up later.
- Set your ASG desired capacity to 0 by running:
aws autoscaling update-auto-scaling-group --auto-scaling-group-name resilient-web-asg --desired-capacity 0 --min-size 0
- This terminates all EC2 instances, stopping per-instance charges.
- The ALB will still charge approximately $0.55/day even with zero instances behind it. To pause completely, also delete the ALB and recreate it later.
- To resume later, update the ASG minimum and desired capacity back to 2.
Delete - I don't want to use this again
Remove all project resources and start fresh. Delete resources in this order to avoid dependency errors.
- Delete the Auto Scaling Group (this terminates all instances automatically) by running:
aws autoscaling delete-auto-scaling-group --auto-scaling-group-name resilient-web-asg --force-delete
- Wait about 1 minute for instances to terminate. Then delete the Load Balancer by running:
aws elbv2 delete-load-balancer --load-balancer-arn [[ALB_ARN="your ALB ARN from the Load Balancers page"]]
- Delete the Target Group by running:
aws elbv2 delete-target-group --target-group-arn [[TG_ARN="your target group ARN from the Target Groups page"]]
- Delete the Launch Template by running:
aws ec2 delete-launch-template --launch-template-name resilient-web-template
- Delete the Security Group (all instances must be terminated first) by running:
aws ec2 delete-security-group --group-name web-sg
- Delete the CloudWatch alarm by running:
aws cloudwatch delete-alarms --alarm-names ScaleOutAlarm
- Delete the SNS topic by running:
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your scaling-alerts topic ARN from the SNS console"]]
Find your ARNs quickly
To find your ALB ARN, run:
aws elbv2 describe-load-balancers --names resilient-web-alb --query "LoadBalancers[0].LoadBalancerArn" --output text
To find your Target Group ARN, run:
aws elbv2 describe-target-groups --names resilient-web-tg --query "TargetGroups[0].TargetGroupArn" --output text
To find your SNS Topic ARN, run:
aws sns list-topics --query "Topics[?ends_with(TopicArn,'scaling-alerts')].TopicArn" --output text
Nice Work!
Nice Work!
You just built a production-grade resilient architecture on AWS. Your web application now heals itself from instance failures, distributes traffic across multiple Availability Zones, and scales automatically with demand.
You've learned how to:
- Build a self-healing Auto Scaling Group that detects instance failures and automatically launches replacements, proving resilience firsthand by terminating instances and watching recovery.
- Deploy an Application Load Balancer across multiple Availability Zones to give clients a single stable DNS endpoint with zero-downtime failover.
- Configure CloudWatch alarms and dynamic scaling policies that add capacity when CPU spikes and remove it when load drops.
- (Secret Mission) Wire SNS email notifications to Auto Scaling events for real-time observability into your infrastructure's scaling behavior.
Ready to quiz yourself?