A hands-on walkthrough of building a web, app, and database tier across two Availability Zones on AWS.
tags: aws, devops, cloud, beginners
Author: Vishal Patil | Project Manager: Ulhas Narwade
vishalcloudjourney
Introduction
Early in my AWS learning, I practiced with single services: an EC2 instance here, an S3 bucket there. Those taught me the basics, but not how real applications are built. Production systems are layered, with clear security boundaries and a plan for failure.
So I built a 3-tier web application on AWS from scratch, with a web tier, an application tier, and a database tier. Each tier has its own subnets and security rules, and everything is spread across two Availability Zones so one data center failing doesn't take the app down.
What Is a 3-Tier Architecture?
| Tier | Responsibility | Network placement |
|---|---|---|
| Web | Serves the React UI, forwards API calls | Public subnets |
| Application | Business logic, database access | Private subnets |
| Database | Persistent data storage | Private subnets |
Why use it?
- Only the web tier faces the internet
- Each tier scales independently
- You can update one layer without touching the others
- A failure in one layer stays contained
Architecture Overview
- A custom VPC across two Availability Zones (AZ1 and AZ2)
- An Internet Gateway for internet connectivity
- An external Application Load Balancer for user traffic
- Web tier: two EC2 instances in public subnets, one per AZ
- An internal Application Load Balancer between the web and app tiers
- App tier: two EC2 instances in private subnets, one per AZ
- Database tier: an Amazon Aurora primary (writer) and a Read Replica in the other AZ, both private
- A NAT Gateway so private instances can download packages without being exposed
Traffic flow:
- The user opens the external ALB's DNS name.
- The request passes through the Internet Gateway to the external ALB.
- The ALB forwards it to a healthy web tier instance.
- Nginx serves the React UI and proxies API calls to the internal ALB.
- The internal ALB routes them to a healthy app tier instance.
- The app tier reads from and writes to Aurora.
- The response returns along the same path.
I built everything in the Asia Pacific (Mumbai) ap-south-1 region.
Step 1: Create the Network
-
VPC with a CIDR block such as
10.0.0.0/16 -
Six subnets, two per tier, one in each AZ (names like
web-public-1a,app-private-1a,db-private-1ahelp a lot) - Internet Gateway attached to the VPC
-
Public route table:
0.0.0.0/0to the Internet Gateway, for the public subnets - NAT Gateway in a public subnet with an Elastic IP
-
Private route table:
0.0.0.0/0to the NAT Gateway, for the private subnets
Step 2: Configure Security Groups
I chained security groups so each tier only trusts the tier in front of it.
| Security group | Inbound rule | Source |
|---|---|---|
| External ALB SG | HTTP 80 | Anywhere |
| Web tier SG | HTTP 80 | External ALB SG |
| Internal ALB SG | HTTP 80 | Web tier SG |
| App tier SG | App port (e.g. 3200) | Internal ALB SG |
| Database SG | 3306 | App tier SG |
For admin access I used SSH from my own IP on the web tier as a jump host. In production, use AWS Systems Manager Session Manager instead.
Step 3: Build the Database Tier
- Create a DB subnet group with the two private database subnets.
- Launch an Aurora (MySQL-compatible) cluster with a writer in AZ1 and a reader in AZ2, using the database security group, with public access set to No.
- From an app instance, connect to the writer endpoint and create the schema:
CREATE DATABASE webappdb;
USE webappdb;
CREATE TABLE transactions (
id INT NOT NULL AUTO_INCREMENT,
amount DECIMAL(10,2),
description VARCHAR(100),
PRIMARY KEY (id)
);
Step 4: Build the Application Tier
Launch two Amazon Linux EC2 instances in the private app subnets and install the tools:
sudo yum update -y
sudo yum install mysql -y
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 16
npm install -g pm2
Set the database details in the backend config (placeholders only, never publish real credentials):
module.exports = {
DB_HOST: '<aurora-writer-endpoint>',
DB_USER: '<db-username>',
DB_PWD: '<db-password>',
DB_DATABASE: 'webappdb'
};
Start the service with PM2 so it survives crashes and reboots:
cd ~/app-tier && npm install
pm2 start index.js
pm2 startup && pm2 save
curl http://localhost:3200/health
Then create a target group with a /health check, register both instances, and create the internal ALB across the private app subnets.
Step 5: Build the Web Tier
On two EC2 instances in the public subnets, build the React app and install Nginx:
cd ~/web-tier
npm install && npm run build
sudo amazon-linux-extras install nginx1 -y
Nginx serves the build and reverse-proxies API calls to the internal ALB:
server {
listen 80;
location / {
root /home/ec2-user/web-tier/build;
index index.html;
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://<internal-alb-dns-name>:80/;
}
}
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl enable nginx
Finally, create a web target group (health check on /), register both instances, and create the internet-facing ALB across the public subnets.
Step 6: Test the Application
I opened the external load balancer's DNS name:
- Home page: "AWS 3-TIER WEB APP DEMO" loaded with the architecture diagram, confirming the web tier and external ALB worked.
- Database page: the "AURORA DATABASE DEMO PAGE" listed records (ID, Amount, Description) with ADD and DEL buttons. I added a record, refreshed, and it persisted.
That one page proved the full chain: browser, external ALB, web tier, internal ALB, app tier, Aurora.
High Availability in Practice
- An instance fails: health checks remove it and the other instance serves users.
- An entire AZ fails: each tier has an instance in the second AZ, so traffic continues.
- The Aurora primary fails: the read replica can be promoted to writer.
Try it: stop one web instance and keep refreshing the page. The site stays up.
Challenges and Fixes
- Unhealthy targets: wrong health check path and a missing security group rule. Check path, port, and security group, in that order.
-
403 Forbidden from Nginx: Nginx couldn't read the build folder. Fixing permissions and the
rootpath solved it. -
Frontend couldn't reach the backend:
proxy_passpointed to the wrong address. Use the internal ALB DNS name. - Database connection errors: wrong endpoint and a missing rule for port 3306 from the app tier SG.
My best debugging habit: test one tier at a time, bottom up.
Key Learnings
- Only the web tier is public, and chained security groups limit each tier's trust.
- Load balancer health checks are the basis of self-healing.
- Subnet and route table design prevents most networking headaches.
- Nginx as a reverse proxy gives the browser one endpoint while traffic routes internally.
- Secrets don't belong in code. Use Secrets Manager or Parameter Store in production.
Cost and Clean-Up
Delete in this order to avoid charges:
- Both load balancers and target groups
- EC2 instances
- The Aurora cluster (reader first, then writer)
- The NAT Gateway, then release its Elastic IP
- Subnets, route tables, Internet Gateway, security groups, and finally the VPC
The NAT Gateway, Aurora, and load balancers cost the most. Set up an AWS Budget alert before you start.
What's Next
- Auto Scaling Groups for the web and app tiers
- HTTPS with AWS Certificate Manager
- Route 53 for a friendly domain name
- AWS WAF in front of the external ALB
- CloudWatch dashboards and alarms
- CloudFormation or Terraform to automate the whole build
Conclusion
Studying VPC, EC2, ELB, security groups, and Aurora separately gave me pieces of a puzzle. Wiring them into one working system showed me how they depend on each other and why networking, security, and availability decisions matter. If you're learning AWS, build a project like this end to end. You'll learn more from debugging it than from any slide deck.
Acknowledgement
This project was completed under the guidance of my project manager, Ulhas Narwade Sir. His direction, reviews, and support helped me understand how a real-world 3-tier architecture is planned, built, and tested on AWS.
Written by: Vishal Patil
Project Manager: Ulhas Narwade
Connect with me:
- LinkedIn: vishalpatil-devops-aws
- dev.to: @vishal_patil_aws
- GitHub: VishalPatil-DevOps-AWS
Top comments (0)