Multi-Tier High Availability on AWS
Deploy a production-style VPC with load balancing, auto scaling, and RDS.
Introduction
30 Second Summary
Think about the last time a website you were using went down. Behind the scenes, that outage likely happened because a single server failed with no backup ready to take over.
In this project, you will build a highly available, multi-tier application platform on AWS. Your platform will spread a containerized web app across two Availability Zones, front it with a load balancer, and isolate a database in private subnets that are invisible to the public internet.
What You'll Build
Open your load balancer's URL in a browser and you'll see a live status page where each refresh shows a different hostname, proving traffic flows across two Availability Zones to Docker containers running in private subnets.
By the end of this project, you'll have:
- A live web app served through an Application Load Balancer where refreshing the page shows different hostnames, proving traffic distributes across Docker containers in two Availability Zones.
- Self-healing infrastructure powered by an Auto Scaling Group that detects failed instances and launches replacements with zero manual intervention.
- An RDS MySQL database locked inside private subnets, completely invisible to the public internet and accepting connections only from your application tier.
- Secret Mission: Terminate a running instance and watch the architecture heal itself with zero downtime.
Are there any prerequisites?
You need an AWS account and a Linux machine (physical or virtual). This project costs approximately $0.50 to $1.00 because NAT Gateways are not covered by the AWS Free Tier, so tear down your resources promptly when finished.
Before We Start
Before you start building, let's lock in what you're about to create and why it matters.
You're going to design and deploy a highly available, multi-tier application platform on AWS. That means building a secure network with public and private subnets, running Docker containers behind a load balancer that distributes traffic across two Availability Zones, and isolating a MySQL database so it is completely unreachable from the public internet.
Set Up the AWS CLI and IAM Access
You have an AWS account, but right now there is no way to interact with AWS from your terminal. Every resource you build in this project lives in your AWS account, so you need a command-line tool that can talk to it.
The AWS CLI lets you run commands against your AWS account directly from the terminal. In this step, you will install it, create an IAM user with the right permissions, and verify the connection is working.
In this step, get ready to:
- Install AWS CLI v2 on your Linux machine.
- Create an IAM user with programmatic access.
- Verify the CLI is connected to your AWS account.
Install the AWS CLI v2
The AWS CLI v2 is the tool you will use throughout this project to verify resources, check connectivity, and interact with your AWS account.
- Check whether the AWS CLI is already installed by running this command:
aws --version
✔️ I see aws-cli/2.x.x
Your AWS CLI v2 is ready. Skip ahead to the next substep.
ⓧ I see aws-cli/1.x.x
You have an older version of the CLI installed. Version 1 is missing features this project uses. Install v2 alongside it by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
- After the install finishes, close and reopen your terminal.
- Verify the new version by running:
aws --version
You should now see a version starting with aws-cli/2.
ⓧ Command not found
The CLI is not installed yet. Install it by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
- After the install finishes, close and reopen your terminal.
- Verify the installation by running:
aws --version
You should see a version starting with aws-cli/2.
Still seeing command not found?
The install script may have placed the aws binary in a location not on your PATH. Try running /usr/local/bin/aws --version to confirm it installed. If that works, add /usr/local/bin to your PATH.
Help me troubleshoot the AWS CLI install on Linux
Create an IAM user and configure the CLI
The AWS CLI needs credentials to prove it is allowed to manage resources in your account. You will create a dedicated IAM user with admin permissions and generate an access key for it.
What is an IAM user?
An IAM user is an identity inside your AWS account with specific permissions. Instead of using your root account credentials (which have unrestricted access), you create an IAM user with only the permissions you need.
For this learning project, you will attach the AdministratorAccess policy so the CLI can create any resource. In production, you would scope permissions down to only what is required.
- Sign in to the AWS Management Console at console.aws.amazon.com.
- In the search bar at the top, type IAM and select the IAM service.
- In the left navigation pane, click Users.
- Click Create user.
- In the User name field, enter cli-admin.
- Click Next.
You are now on the Set permissions page.
- Under Permissions options, select Attach policies directly.
- In the search box under Permissions policies, type AdministratorAccess.
- Select the checkbox next to AdministratorAccess.
- Click Next.
- On the Review and create page, click Create user.
Your IAM user is created. Now you need to generate an access key so the CLI can authenticate as this user.
- Click on the user name cli-admin in the Users list to open the user details page.
- Select the Security credentials tab.
- In the Access keys section, 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 credentials now
You are now on the Retrieve access key page. This is the only time your Secret Access Key is visible. Click Download .csv file to save both keys to a file on your machine, or copy them somewhere safe.
Do not close this page until you have saved both keys. If you lose the Secret Access Key, you will need to delete this key and create a new one.
Now connect the CLI to your AWS account using those credentials.
- Configure the CLI by running this command:
aws configure
The CLI prompts you for four values. Enter them one at a time, pressing Enter after each:
- At the AWS Access Key ID prompt, paste your Access Key ID.
- At the AWS Secret Access Key prompt, paste your Secret Access Key.
- At the Default region name prompt, enter us-east-1.
- At the Default output format prompt, enter json.
What does aws configure do?
This command saves your credentials to ~/.aws/credentials and your settings to ~/.aws/config. Every aws command you run from now on uses these values automatically.
The region us-east-1 is where all your resources will be created. The json output format makes command results easy to read in the terminal.
If the command completes without any output, that means it worked.
Verify the CLI connection
You have installed the CLI and entered your credentials. The final check is to confirm the CLI can actually reach your AWS account and authenticate successfully.
Before you run this next command, what do you think it will return? Will it succeed, or will it fail?
- Test the connection by running this command:
aws sts get-caller-identity
You should see JSON output containing three fields: your Account ID, your User ARN (which includes cli-admin), and a UserId.
What does this command do?
The aws sts get-caller-identity command asks AWS "who am I?" It returns the account and identity that the CLI is authenticated as. Seeing your account ID and the cli-admin user ARN confirms everything is wired up correctly.
✔️ I see my account ID and user ARN
Your CLI is authenticated and ready to go.
ⓧ I see an error
Common causes:
- Double check that you pasted the correct Access Key ID and Secret Access Key. Run aws configure again and re-enter them.
- Make sure you did not accidentally paste extra spaces before or after the key values.
- Confirm the IAM user cli-admin has the AdministratorAccess policy attached in the IAM console.
Help me troubleshoot aws sts get-caller-identity errors
Your CLI is connected to your AWS account and ready to build infrastructure. Next up, you will create the secure network foundation that every service in your platform will live in.
Build the Secure Network Foundation
Your AWS CLI is connected and your credentials are verified. Now it's time to build the network that every other service in this project will live inside.
Every AWS resource you deploy needs a VPC to call home. Right now, you have nothing. In this step, you'll use the VPC creation wizard to build your entire network topology in one shot, then lock down traffic between tiers with security groups.
In this step, get ready to:
- Create a VPC with public and private subnets across two Availability Zones.
- Trace the route tables to understand how traffic flows between subnets and gateways.
- Create three security groups to enforce least-privilege traffic between your application tiers.
Create the VPC with subnets and gateways
The VPC wizard creates your subnets, route tables, an Internet Gateway, and NAT Gateways all in one operation. You configure the topology you want and AWS builds it for you.
- In the AWS Management Console, check that the region selector in the top-right corner shows US East (N. Virginia) us-east-1. If it shows a different region, click it and select US East (N. Virginia).
- Type VPC in the search bar at the top and select VPC from the results.
- Click Create VPC.
- Under Resources to create, select VPC and more.
- For Name tag auto-generation, enter ha-platform.
- Set Number of Availability Zones to 2.
- Set Number of public subnets to 2.
- Set Number of private subnets to 2.
What do these settings do?
Two Availability Zones means your infrastructure spans two physically separate data centers. If one goes down, the other keeps running.
Public subnets get a direct route to the internet through the Internet Gateway. Private subnets reach the internet through a NAT Gateway, which allows outbound traffic but blocks anything inbound.
- Under NAT gateways, select 1 per AZ.
- Leave VPC endpoints at its default (S3 Gateway).
- Click Create VPC.
Cost warning
NAT Gateways are not covered by the AWS Free Tier. Each one costs approximately $0.045 per hour, so two NAT Gateways cost about $2.16 per day.
Complete and tear down this project within 2 hours to keep costs under $1. The Before You Go section has full teardown instructions.
A status page appears showing each resource being created. Wait until every item shows a green checkmark before continuing.
Seeing a creation error?
If you see an error about insufficient Elastic IP addresses, your account may have hit the default limit of 5 EIPs per region. Navigate to the Elastic IPs section in the VPC console and release any unused Elastic IPs, then try again.
If you see a different error, double-check that your region is set to us-east-1 and that your IAM user has the AdministratorAccess policy attached.
Still stuck? Help me troubleshoot my VPC creation error.
Trace the network topology
The wizard built your entire network in one shot. Before moving on, trace the route tables to confirm traffic flows correctly between your subnets and gateways.
- Click View VPC on the success page to open your new VPC.
- Click the Resource map tab to see the visual topology of your network.
You should see the Internet Gateway on the left, two public subnets, two private subnets, and two NAT Gateways.
- Click on one of the public subnet route tables in the resource map. Confirm it has a route sending 0.0.0.0/0 to the Internet Gateway.
- Click on one of the private subnet route tables. Confirm it has a route sending 0.0.0.0/0 to a NAT Gateway.
Why two different routes?
Public subnets route internet traffic directly through the Internet Gateway, so resources inside them can be reached from the outside. Private subnets route through a NAT Gateway instead, which lets instances reach the internet for updates but blocks all inbound connections.
This separation is the foundation of network isolation. Your load balancer will sit in public subnets, while your application instances and database stay hidden in private subnets.
Create the security groups
Security groups act as virtual firewalls that control which traffic can flow between your tiers. You need three: one for the load balancer, one for the application instances, and one for the database.
Each group only allows traffic from the tier directly above it, creating a chain of trust.
- In the VPC console left sidebar, under Security, click Security Groups.
- Click Create security group.
- For Security group name, enter ALB-sg.
- For Description, enter Allow HTTP from internet.
- For VPC, remove the default VPC and select your ha-platform VPC.
- Under Inbound rules, click Add rule.
- Set Type to HTTP and Source to Anywhere-IPv4 (0.0.0.0/0).
- Click Create security group.
What does ALB-sg allow?
This security group allows anyone on the internet to send HTTP traffic (port 80) to your Application Load Balancer. It is the only entry point into your platform.
You should see the ALB-sg detail page confirming it was created. Now create the second security group for your application instances.
- In the left sidebar, click Security Groups to return to the list.
- Click Create security group.
- For Security group name, enter app-sg.
- For Description, enter Allow HTTP from ALB only.
- For VPC, remove the default VPC and select your ha-platform VPC.
- Under Inbound rules, click Add rule.
- Set Type to HTTP. Set Source to Custom, then search for and select ALB-sg.
- Click Create security group.
The app-sg detail page confirms it was created. Now create the final security group for the database.
- In the left sidebar, click Security Groups to return to the list.
- Click Create security group.
- For Security group name, enter rds-sg.
- For Description, enter Allow MySQL from app only.
- For VPC, remove the default VPC and select your ha-platform VPC.
- Under Inbound rules, click Add rule.
- Set Type to MYSQL/Aurora. Set Source to Custom, then search for and select app-sg.
- Click Create security group.
How does this chain of trust work?
Traffic can only flow in one direction through the chain. The internet can reach the ALB (ALB-sg allows port 80 from anywhere). The ALB can reach the app instances (app-sg allows port 80 only from ALB-sg). The app instances can reach the database (rds-sg allows port 3306 only from app-sg).
Nothing else gets through. The database cannot be reached from the internet, and the app instances cannot be reached directly either.
Before you move on, what do you expect to see if you go back to the Security Groups list and filter by the ha-platform VPC?
- Click Security Groups in the left sidebar to return to the full list.
- In the search bar, type ha-platform to filter the results.
You should see at least four security groups: the default one that AWS creates with every VPC, plus your three custom groups (ALB-sg, app-sg, rds-sg).
- Click on ALB-sg and check the Inbound rules tab. Confirm it shows HTTP on port 80 from 0.0.0.0/0.
- Go back and click on app-sg. Confirm its inbound rule shows HTTP on port 80 from ALB-sg.
- Go back and click on rds-sg. Confirm its inbound rule shows MYSQL/Aurora on port 3306 from app-sg.
Security group missing or wrong VPC?
If you see fewer than three custom security groups, you may have accidentally created one in the default VPC instead of the ha-platform VPC. Delete the misplaced security group and recreate it, making sure to select the ha-platform VPC in the VPC dropdown.
If the inbound rules look wrong, click Edit inbound rules on the security group's Inbound rules tab to fix them without recreating the group.
Still stuck? Help me fix my security group configuration.
Your network foundation is complete. You have a production-style VPC with isolated tiers and security groups that enforce exactly which traffic can flow between them. Next up, you'll put something inside this network by launching a containerized app into the private subnets.
Launch the App into Private Subnets
In the last step, you built a VPC with public and private subnets, NAT Gateways, and security groups that enforce traffic rules between tiers. Your network is ready, but nothing is actually running inside it.
In this step, you'll put a containerized web app into the private subnets using a launch template. The launch template defines everything an instance needs to start up, including a script that installs Docker and runs a web server automatically at boot.
In this step, get ready to:
- Create an IAM role that enables secure instance access through SSM Session Manager.
- Build an EC2 launch template with a Docker-based User Data script.
- Launch a test instance and verify the web server is running.
Create an IAM role for SSM Session Manager
SSM Session Manager lets you connect to EC2 instances without opening SSH ports or managing SSH keys. For it to work, your instances need an IAM role with the right permissions attached.
- Search for IAM in the AWS Console search bar and open the IAM service.
- Click Roles in the left sidebar.
- Click Create role.
- Under Trusted entity type, select AWS service.
- Under Use case, select EC2.
- Click Next.
- In the search bar, type AmazonSSMManagedInstanceCore and select the checkbox next to it.
- Click Next.
- Enter ec2-ssm-role in the Role name field.
- Click Create role.
You should see a success banner confirming that the role ec2-ssm-role was created.
What about the instance profile?
When you create an IAM role for EC2 through the console, AWS automatically creates an instance profile with the same name. The instance profile is what actually attaches the role to an EC2 instance.
You'll see ec2-ssm-role appear as an option when you configure your launch template in the next substep.
Why SSM instead of SSH?
Traditional SSH requires opening port 22 to the internet, managing key pairs, and sometimes running a bastion host. SSM Session Manager eliminates all of that.
AWS lists SSH/RDP as a common anti-pattern in the Well-Architected Framework and recommends SSM Session Manager as the alternative. It gives you a secure browser-based shell with no inbound ports needed.
Don't see the success banner?
Make sure you selected EC2 under Use case, not another AWS service.
Check that the policy name is exactly AmazonSSMManagedInstanceCore. A similar-sounding policy exists but provides different permissions.
help me troubleshoot creating an IAM role for SSM Session Manager in the AWS Console
Build the EC2 launch template
A launch template defines everything an EC2 instance needs to start up: the operating system image, instance size, security group, IAM role, and a startup script called User Data. You'll create one template now and reuse it to launch every instance in this project.
- Search for EC2 in the AWS Console search bar and open the EC2 service.
- Click Launch Templates in the left sidebar.
- Click Create launch template.
- Enter ha-platform-lt in the Launch template name field.
- Under Application and OS Images, search for and select Amazon Linux 2023 AMI.
- Under Instance type, select t3.micro (free tier eligible).
- Under Network settings, click Select existing security group and choose app-sg.
Now you'll configure the advanced settings that give the instance its IAM role and startup script.
- Expand the Advanced details section at the bottom of the page.
- In the IAM instance profile dropdown, select ec2-ssm-role.
- Scroll down to the User data field and paste the following script:
#!/bin/bash
yum update -y
yum install -y docker
service docker start
docker run -d -p 80:80 public.ecr.aws/amazonlinux/amazonlinux:2023 bash -c "yum install -y httpd && echo '<h1>Hello from $(hostname)</h1>' > /var/www/html/index.html && httpd -DFOREGROUND"
What does this User Data script do?
User Data scripts run automatically the first time an EC2 instance boots. This script does four things in order:
- yum update -y updates all system packages to their latest versions.
- yum install -y docker installs Docker on the instance.
- service docker start starts the Docker service so containers can run.
- The docker run command pulls a public Amazon Linux container image, installs the Apache web server inside it, and serves a page showing the instance's hostname on port 80.
- Click Create launch template.
You should see a success message confirming that ha-platform-lt was created.
Don't see ec2-ssm-role in the dropdown?
The instance profile can take a minute to appear after role creation. Try refreshing the page and expanding Advanced details again.
If it still doesn't appear, go back to the IAM console and confirm that ec2-ssm-role shows EC2 as the trusted entity. If it shows a different service, delete the role and recreate it with EC2 selected.
help me troubleshoot my IAM instance profile not appearing in the EC2 launch template dropdown
Launch a test instance and connect via SSM
Your template is ready. Now you'll launch a single instance from it in a private subnet and connect to it using SSM Session Manager to verify the web server is running.
- In the Launch Templates list, select ha-platform-lt.
- Click Actions, then select Launch instance from template.
- Under Network settings, select your ha-platform VPC.
- In the Subnet dropdown, select one of the private subnets.
- Confirm that Auto-assign public IP is set to Disable. Private subnets default to this, but verify it.
- Click Launch instance.
You should see a success message. The instance needs 2-3 minutes to boot, run the User Data script, and register with SSM.
- Click Instances in the left sidebar to view your running instances.
- Wait until the instance's Instance state shows Running and Status check shows 2/2 checks passed.
Be patient with the status checks
The User Data script installs Docker and pulls a container image, which takes a few minutes. If the status checks haven't passed after 3 minutes, click the refresh icon in the EC2 Instances table.
Now connect to the instance using SSM Session Manager to confirm the web server is running.
- Select your running instance in the EC2 Instances list.
- Click Connect.
- Select the Session Manager tab.
- Click Connect.
A browser-based terminal session opens. Before you run the next command, think about what you expect to see. The User Data script installed Docker and started a web container on port 80. What should curl localhost return?
- Run the following command in the SSM session:
curl localhost
You should see HTML output containing <h1>Hello from followed by the instance's hostname. The Docker container is serving web traffic on port 80.
Seeing 'Connection refused' or no output?
The User Data script may still be running. Wait another minute and try curl localhost again.
If it still fails, check whether Docker is running by entering docker ps in the SSM session. If no containers are listed, the User Data script may have had an error. Terminate this instance, double check the User Data in your launch template, and launch a new one.
If Session Manager won't connect at all, confirm your instance has the ec2-ssm-role IAM role attached and that the private subnet routes outbound traffic through a NAT Gateway (SSM needs outbound internet access to reach the Systems Manager endpoints).
help me troubleshoot my Docker container not starting on an EC2 instance launched with User Data
The web server works from inside the instance. Now, what do you think will happen if you try to reach this instance from your browser? Take a look at the instance details in the EC2 console. Notice that the Public IPv4 address field is empty.
- Copy the instance's Private IPv4 address from the instance details.
- Paste it into your browser's address bar and press Enter.
The page never loads. Your browser sits and spins with no response.
Why can't you reach a working web server?
Your instance lives in a private subnet with no public IP. Private IP addresses like 10.0.x.x are not routable over the internet. Your browser has no path to reach the instance.
This is network isolation working exactly as designed. The app runs, but only resources inside the VPC can talk to it. To make it reachable from the internet, you need something sitting in the public subnets that forwards traffic inward. That is exactly what the next step builds.
Your app runs inside the private subnet, and you've proven it works. But nobody outside the network can reach it, and a single instance means one failure takes the whole app down. In the next step, you'll solve both problems with a load balancer and auto scaling.
Add Load Balancing and Auto Scaling
In the last step, you launched a Docker container into a private subnet and proved it responds through SSM Session Manager. You ran curl localhost and saw the page. But you also hit a wall: there is no way to reach the app from a web browser.
Your app also runs on just one instance. If that instance goes down, the app goes with it. This step solves both problems. An Application Load Balancer gives the app a public entry point through the public subnets. An Auto Scaling Group keeps at least two instances running across both Availability Zones.
In this step, get ready to:
- Create an Application Load Balancer and Target Group to route internet traffic to your app.
- Configure an Auto Scaling Group to maintain two healthy instances across both Availability Zones.
- Verify traffic distribution by opening the ALB URL and watching hostnames alternate.
Create the Target Group and Application Load Balancer
The ALB needs a Target Group to know where to send traffic. You will create the Target Group first, then the ALB itself.
- In the EC2 console, click Target Groups in the left sidebar under Load Balancing.
- Click Create target group.
- Select Instances as the target type.
- Enter ha-platform-tg in the Target group name field.
- Select ha-platform from the VPC dropdown.
- Confirm the Health check path is set to /.
- Click Next.
- Click Create target group without registering any targets.
What is a Target Group?
A Target Group tells the load balancer which instances should receive traffic. The health check on / means the ALB will periodically send a test request to each instance's root path.
If an instance stops responding, the ALB automatically stops routing traffic to it. You skipped registering targets because the Auto Scaling Group will handle that for you.
With the Target Group ready, create the load balancer that will sit in front of your app.
- Click Load Balancers in the left sidebar.
- Click Create load balancer.
- Under Application Load Balancer, click Create.
- Enter ha-platform-alb in the Load balancer name field.
- Under Scheme, select Internet-facing.
- Under Network mapping, select ha-platform as the VPC.
- Check the boxes for both public subnets (one in each Availability Zone).
- Under Security groups, remove the default security group.
Why public subnets for the ALB?
The ALB sits in the public subnets so it can receive traffic from the internet. It then forwards requests to instances in the private subnets. This is what makes your private instances reachable without giving them public IPs.
- Select ALB-sg from the security group dropdown.
- Under Listeners and routing, confirm the listener is set to HTTP on port 80.
- Select ha-platform-tg as the Default action for the listener.
- Click Create load balancer.
- Return to the Load Balancers page and wait for the State column to show Active (this takes 2-3 minutes).
ALB stuck in Provisioning?
Give it up to 5 minutes. If the state does not change, check that you selected the two public subnets (not private) and that ALB-sg allows inbound HTTP on port 80 from 0.0.0.0/0.
If you selected the wrong subnets, delete the ALB and recreate it with the correct public subnets.
Help me troubleshoot my ALB not becoming active.
Create the Auto Scaling Group
The ALB can now receive traffic from the internet, but there are no instances registered in the Target Group. An Auto Scaling Group will launch instances from your launch template and register them with the Target Group automatically.
- In the EC2 console, click Auto Scaling Groups in the left sidebar under Auto Scaling.
- Click Create Auto Scaling group.
- Enter ha-platform-asg in the Auto Scaling group name field.
- Select ha-platform-lt from the Launch template dropdown.
- Click Next.
- Select ha-platform from the VPC dropdown.
- Select both private subnets from the Availability Zones and subnets list.
- Click Next.
Why private subnets for the instances?
The instances sit in private subnets so they are not directly reachable from the internet. All traffic flows through the ALB in the public subnets. The instances reach the internet for updates through the NAT Gateways you created in Step 2.
- Select Attach to an existing load balancer.
- Select Choose from your load balancer target groups.
- Select ha-platform-tg from the list.
- Under Health checks, check Turn on Elastic Load Balancing health checks.
- Click Next.
Why enable ELB health checks?
By default, the Auto Scaling Group only checks whether the EC2 instance itself is running. With ELB health checks enabled, the ASG also checks whether the Target Group considers the instance healthy (responding on port 80).
If the web server inside a container crashes but the EC2 instance stays running, this setting lets the ASG detect the problem and replace the instance.
- Set Desired capacity to 2.
- Set Min desired capacity to 2.
- Set Max desired capacity to 4.
- Under Automatic scaling, select Target tracking scaling policy.
- Set the Target value to 70 (this targets 70% average CPU utilization).
- Click Next.
- Click Next to skip the Notifications page.
- Click Next to skip the Tags page.
- Review your settings and click Create Auto Scaling group.
What does the scaling policy do?
The Target Tracking scaling policy monitors average CPU utilization across your instances. If the average goes above 70%, the ASG launches more instances (up to your maximum of 4) to handle the load. When CPU drops back down, the ASG terminates the extras.
The minimum of 2 instances ensures your app stays available even if one instance fails. One instance runs in each Availability Zone.
- Click on ha-platform-asg to open the ASG details.
- Click the Instance management tab to see your instances launching.
You should see two instances with a Lifecycle state of Pending or InService. Wait until both show InService before continuing (this takes 2-3 minutes).
- Navigate to Target Groups in the left sidebar.
- Click on ha-platform-tg and open the Targets tab.
You should see two registered targets with a Health status of healthy. If they still show initial, wait another minute and refresh the page.
Instances not becoming healthy?
Check the app-sg security group. It must allow inbound traffic on port 80 from ALB-sg. If it allows traffic from a specific IP instead, the ALB health checks will fail.
Also verify the launch template uses the app-sg security group and the correct Instance Profile. If the User Data script did not run, the web server will not be listening on port 80.
Help me troubleshoot my ASG instances not becoming healthy.
Verify traffic distribution and clean up the test instance
- Navigate to Load Balancers in the left sidebar.
- Click on ha-platform-alb and copy the DNS name from the Description section.
Before you open this URL, think about what you expect to see. Will the page load successfully? If you refresh the page several times, will you always see the same hostname?
- Paste the ALB DNS name into your browser's address bar and press Enter.
You should see a page that says "Hello from" followed by a hostname. This is the Dockerized web server running on one of your instances.
- Refresh the page several times.
The hostname alternates between two different values. Each value is a different EC2 instance running in a different Availability Zone. The ALB is distributing your requests across both instances.
What does the hostname alternating prove?
Each time you refresh, the ALB picks a healthy instance to handle the request. Seeing two different hostnames proves that traffic is balanced across both Availability Zones. If one AZ went down, the ALB would route all traffic to the surviving instance.
The original test instance from Step 3 is still running in a private subnet. It is not part of the Auto Scaling Group and is no longer needed.
- Navigate to Instances in the EC2 console.
- Identify the test instance you launched manually in Step 3 (it is not part of the ha-platform-asg Auto Scaling Group).
- Select the test instance.
- Click Instance state, then Terminate instance.
- Click Terminate to confirm.
Page not loading in the browser?
Make sure both targets in ha-platform-tg show a health status of healthy. If they show unhealthy, the ALB will not route traffic to them.
Also check that you are using http:// in the URL, not https://. The ALB listener is configured for HTTP only.
Help me troubleshoot my ALB not serving the app page.
Your app is now publicly accessible and fault-tolerant. Traffic flows through the ALB to two instances running across different Availability Zones. Next up, you will add a database that is completely hidden from the public internet.
Isolate the Database in Private Subnets
Your app is publicly reachable through the load balancer, with two instances keeping it resilient across Availability Zones. The last missing piece is a database to store your application's data.
A production database must never be accessible from the public internet. In this step, you'll deploy an Amazon RDS MySQL instance inside your private subnets, locked down by the rds-sg security group so only your app instances can reach it.
In this step, get ready to:
- Create an RDS Subnet Group to place the database in your private subnets.
- Launch a free-tier-eligible RDS MySQL instance with public access disabled.
- Verify database connectivity from an app instance using SSM Session Manager.
Create the RDS subnet group
An RDS Subnet Group tells AWS which subnets your database can be placed in. By selecting only your private subnets, you ensure the database never receives a public IP address.
- In the AWS Console search bar, type RDS and open the RDS console.
- In the left sidebar, click Subnet groups.
- Click Create DB Subnet Group.
- Set the Name to ha-platform-db-subnets.
- Enter a Description such as Private subnets for RDS.
- Under VPC, select your ha-platform VPC.
- Under Add subnets, select both Availability Zones from the Availability Zones dropdown.
- In the Subnets dropdown for each AZ, select only the private subnet.
Identifying your private subnets
If you named your VPC ha-platform during the VPC wizard, your private subnets will contain private in their name. Select those and leave the public subnets unchecked.
- Click Create.
You should see ha-platform-db-subnets appear in the Subnet groups list with a status of Complete.
Don't see your subnet group?
Make sure you selected the ha-platform VPC. The subnet list only shows subnets belonging to the selected VPC.
You need at least two subnets in different Availability Zones. If you only see one AZ, check that both private subnets were created by the VPC wizard in Step 2.
Help me troubleshoot my RDS Subnet Group creation.
Create the RDS MySQL instance
With the subnet group ready, you can launch the database itself. You'll use a free-tier-eligible db.t3.micro instance with a Single-AZ deployment to keep costs at zero.
- In the RDS sidebar, click Databases.
- Click Create database.
- Under creation method, select Standard create.
- Under Engine type, select MySQL.
- Under Templates, select Free tier.
Why Single-AZ?
The Free tier template automatically sets the deployment to Single-AZ. Multi-AZ deployments are not covered by the AWS Free Tier.
Your compute tier already has multi-AZ resilience through the Auto Scaling Group, so you still have high availability where it matters most for this project.
- Under Settings, enter a DB instance identifier such as ha-platform-db.
- Set the Master username to admin.
- Enter a Master password you will remember, then confirm it.
Remember your password
You will need this password to connect to the database later in this step. Write it down somewhere safe before continuing.
- Under Instance configuration, confirm the instance class is db.t3.micro.
- Scroll down to the Connectivity section.
- Under Virtual Private Cloud (VPC), select your ha-platform VPC.
- Under DB subnet group, select ha-platform-db-subnets.
- Set Public access to No.
- Under VPC security group (firewall), choose Choose existing and select rds-sg.
- Remove any default security group so only rds-sg is assigned.
What does setting Public access to No actually do?
Setting this to No prevents AWS from assigning a public IP to the database. Combined with the private subnet placement and the rds-sg security group, your database has three layers of isolation from the internet.
- Expand the Additional configuration section.
- Under Backup, confirm Enable automated backups is checked.
- Set the Backup retention period to 7 days.
- Click Create database.
The RDS instance takes 5-10 minutes to launch. You'll see the status change from Creating to Available in the Databases list.
Database stuck in Creating?
RDS provisioning can take up to 10 minutes. Refresh the page periodically. If the status shows an error, check that you selected the correct VPC, subnet group, and security group.
Help me troubleshoot my RDS instance creation.
- Once the status shows Available, click on your database instance name to open its details.
- In the Connectivity & security tab, find the Endpoint value and copy it.
Record the endpoint here: your RDS endpoint
Connect to the database from an app instance
The database is running in a private subnet with no public access. The only way to reach it is from inside your VPC. You'll use SSM Session Manager to connect to one of your app instances and test the database connection from there.
- In the AWS Console search bar, type Systems Manager and open it.
- In the left sidebar, click Session Manager.
- Click Start session.
- Select one of your Auto Scaling instances and click Start session.
- Install the MySQL client by running this command in the session:
sudo yum install -y mariadb105
Why mariadb105?
Amazon Linux 2023 provides the mariadb105 package as a MySQL-compatible client. It includes the mysql command-line tool you need to connect to your RDS MySQL instance.
You should see the installation complete with no errors.
Now think about what you expect to happen next. Your app instance is in a private subnet with the app-sg security group. The RDS instance is also in a private subnet with rds-sg, which allows inbound on port 3306 from app-sg. Do you think the connection will succeed?
- Connect to your RDS instance by running:
mysql -h [[RDS_ENDPOINT="your RDS endpoint"]] -u admin -p
- When prompted, enter the master password you set during database creation.
You should see a MySQL prompt appear, confirming your app instance can reach the database through the private network.
- Verify the connection is working by running this SQL command:
SHOW DATABASES;
You should see a list of default MySQL databases returned. This confirms full connectivity from the app tier to the database tier.
Connection refused or timing out?
Check that the rds-sg security group allows inbound on port 3306 from app-sg. The source must be the security group ID, not an IP address.
Make sure the RDS instance status is Available before attempting to connect. Also verify you copied the full endpoint string including the region suffix.
If you entered the wrong password, you'll see an access denied error. Double check the master password you set during creation.
Help me debug my RDS connection from SSM Session Manager.
Now confirm that the database is truly isolated from the internet. Your connection proves the app tier can reach it. The next checks prove nothing else can.
- Type exit in the MySQL prompt to return to the instance shell.
- Switch back to the RDS console in your browser.
- Click on your database instance name to view its details.
- In the Connectivity & security tab, confirm that Publicly accessible shows No.
- Scroll to the Security group rules section and verify the only inbound rule is port 3306 from app-sg.
Your database has three layers of protection. It lives in a private subnet with no public IP. Its security group only accepts traffic from the app tier. Publicly accessible is set to No.
Secret mission
Break It and Watch It Heal
You have built a platform that claims to be highly available. Now it is time to test that claim. Terminate a running instance and watch your Auto Scaling Group detect the failure, launch a replacement, and restore full capacity with zero downtime visible through the load balancer.
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. This project created AWS resources with ongoing costs, and the two NAT Gateways are the most expensive at approximately $2.16 per day combined.
Cost warning
The two NAT Gateways cost approximately $0.045 per hour each, totalling around $2.16 per day. They are not covered by the AWS Free Tier.
The ALB, EC2 instances, and RDS instance may be covered by free tier depending on your account eligibility and when you created your account. If you are not sure, delete everything to avoid unexpected charges.
Resources you used:
- Auto Scaling Group ha-platform-asg and its EC2 instances.
- Application Load Balancer ha-platform-alb and Target Group ha-platform-tg.
- EC2 Launch Template ha-platform-lt.
- RDS MySQL instance and DB Subnet Group ha-platform-db-subnets.
- VPC ha-platform (includes subnets, route tables, 2 NAT Gateways, Internet Gateway, and security groups).
- Elastic IP addresses associated with the NAT Gateways.
- IAM Role ec2-ssm-role and its Instance Profile.
- IAM User with programmatic access (access key).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue working with this infrastructure soon.
- The two NAT Gateways cost approximately $2.16 per day combined. This is the primary ongoing cost.
- The ALB, EC2 instances, and RDS instance may remain within free tier limits depending on your account eligibility and total usage across your account.
- The AWS CLI remains installed on your Linux machine.
Pause - I'll come back to this later
Shut down the expensive compute and database resources while keeping the VPC as a network foundation for future projects. The VPC, subnets, route tables, and security groups have no ongoing cost.
- In the EC2 console, go to Auto Scaling Groups. Select ha-platform-asg, click Edit, and set desired capacity and minimum capacity to 0. Save and wait for instances to terminate, then delete the group.
- In the EC2 console, go to Load Balancers. Select ha-platform-alb and delete it. Then go to Target Groups and delete ha-platform-tg.
- In the RDS console, go to Databases. Select your MySQL instance and delete it. Uncheck Create final snapshot and check the acknowledgment box.
- In the VPC console, go to NAT Gateways. Delete both NAT Gateways to stop the biggest cost.
- In the VPC console, go to Elastic IPs. Release any Elastic IP addresses that were associated with the NAT Gateways.
- Keep the VPC, subnets, route tables, and security groups as a foundation for future projects.
Delete - I don't want to use this again
Remove all project resources and start fresh. Delete in reverse dependency order to avoid conflicts.
- In the EC2 console, go to Auto Scaling Groups. Select ha-platform-asg, click Edit, and set desired capacity and minimum capacity to 0. Save and wait for all instances to terminate. Then select the group and click Delete.
- In the EC2 console, go to Load Balancers. Select ha-platform-alb and click Actions then Delete load balancer.
- In the EC2 console, go to Target Groups. Select ha-platform-tg and click Actions then Delete.
- In the EC2 console, go to Launch Templates. Select ha-platform-lt and click Actions then Delete template.
- In the RDS console, go to Databases. Select your MySQL instance and click Actions then Delete. Uncheck Create final snapshot and check the acknowledgment box.
- In the RDS console, go to Subnet Groups. Select ha-platform-db-subnets and click Delete.
- In the VPC console, go to Your VPCs. Select ha-platform and click Actions then Delete VPC. This deletes the subnets, route tables, NAT Gateways, Internet Gateway, and security groups.
- In the VPC console, go to Elastic IPs. Release any remaining Elastic IP addresses that were associated with the NAT Gateways. Unreleased Elastic IPs incur charges.
- In the IAM console, go to Roles. Search for ec2-ssm-role and delete it. This also removes the associated Instance Profile.
- In the IAM console, go to Users. Delete the IAM user you created for this project. This also removes its access key.
Nice Work!
Nice Work!
You just built a highly available, multi-tier application platform on AWS. Traffic flows through a load balancer to auto-scaling Docker containers across two Availability Zones, backed by a database that the public internet cannot reach.
What you learned:
- Designed a multi-AZ VPC with public and private subnets, NAT Gateways, and security groups that enforce network isolation between your application and database tiers.
- Launched a containerized application on EC2, fronted by an Application Load Balancer and an Auto Scaling Group that maintains capacity across two Availability Zones.
- Deployed an RDS MySQL database isolated in private subnets with no public access, and managed your instances through SSM Session Manager with zero SSH ports exposed to the internet.
- Secret Mission: Proved the architecture self-heals by terminating an instance and watching the Auto Scaling Group detect the failure, launch a replacement, and restore full capacity with zero downtime.
Ready to quiz yourself?