Last updated: 12th October, 2025
Building A 3-Tier Web Application on AWS
Learn to design a Scalable, Secure, Reliable and Resilient web application using AWS
This project was developed within a lab environment that automatically terminates all resources when lab timer lapses. To ensure continuity, I created an Infrastructure as Code (IaC) solution using CloudFormation. This allowed me to quickly redeploy the entire stack when the lab is restarted and pick up from where I left off without any manual reconfiguration.
Introduction: Initial Setup & Prerequisites
A 3-tier architecture is a proven software design model that separates an application into three distinct layers; presentation, application, and data. This separation improves scalability, security, and maintainability. In this guide, we'll walk through the process of deploying a 3-tier web application architecture on AWS, step by step.
The Presentation Tier:
-The user-facing side of the application, containing web servers which receives and responds to incoming requests
The Application Tier:
-The section where the backend/application logic resides.Data Tier:
-Hosts and manages the application data. Often where the databases are stored.
A three-tier architecture follows the principle of loose coupling, making it easy to replace or upgrade individual components without redesigning the entire system. Applications built with this design are inherently scalable, reliable, resilient, and secure.
Here's the architecture we're going to develop

Prerequisites
- • AWS Account with appropriate IAM users and permissions
- • Familiarity with AWS Management Console
- • A Command Line Interface (CLI) tool
- • Familiarity with Linux Commands
- • Familiarity with AWS CloudFormation (optional but recommended)
I will use a wsl terminal in this guide
Step 1: Creating VPC and Subnets
We’ll start by creating a VPC and subnets to isolate application resources, similar to how networks are segmented in an on-premises environment.
• In the Management Console search for VPC and open the VPC Dashboard.
• Click on Create VPC.
• Choose "VPC only" and provide a name ("webapp-network" in my case).
• Set the IPv4 CIDR block to 10.0.0.0/16
• Review and Click Create VPC


Next we will create the subnets. One each for presentation Tier and Application Tier,
and Two for the data tier for high availability as shown below.

Expand the VPC Dashboard sidebar if closed and go to Subnets
Click on Create Subnet
Choose the VPC created earlier ("webapp-network").
• We will create the Public Subnet first
• Let's name it "public-subnet" since it's the only public subnet
• Set the Availability Zone to your preference (e.g., us-west-2a) and note it somewhere
• Set the IPv4 subnet CIDR block to 10.0.1.0/24
• Review and click Create Subnet


Repeat the process to create the three private subnets
• We'll name them "private-subnet-1", "private-subnet-2", and "private-subnet-3" respectively
or you can go for any naming format that works for you• Put the first 2 private subnets in same Availability Zone as the public subnet
and then put the last subnet in a different Availability Zone
Refer to the image here for clarity and set their CIDRs as shown
Cloudformation Code for Step 1 - (VPC and Subnets)
The part of the Cloudformation code that creates the vpc and subnets as discussed above
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
This template creates a 3 tier web application infrastructure on AWS.
It includes a VPC with public and private subnets across two availability zones,
along with necessary routing, NAT gateways, and network ACLs.
Parameters:
VPCName:
Description: The name of the VPC being created.
Type: String
Default: webapp-network
MyIP:
Description:>-
Enter your IP address with /32 suffix or leave it at default
(e.g., 201.16.145.100/32)
Type: String
Default: 0.0.0.0/0
AL2023AMI:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Mappings:
SubnetConfig:
VPC:
CIDR: 10.0.0.0/16
Public:
CIDR: 10.0.1.0/24
Private1:
CIDR: 10.0.2.0/24
Private2:
CIDR: 10.0.3.0/24
Private3:
CIDR: 10.0.4.0/24
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: "true"
EnableDnsHostnames: "true"
CidrBlock: !FindInMap
- SubnetConfig
- VPC
- CIDR
Tags:
- Key: Name
Value: !Ref VPCName
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Public
- CIDR
Tags:
- Key: Name
Value: public-subnet
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Private1
- CIDR
Tags:
- Key: Name
Value: private-subnet-1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Private2
- CIDR
Tags:
- Key: Name
Value: private-subnet-2
PrivateSubnet3:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Private3
- CIDR
Tags:
- Key: Name
Value: private-subnet-3
Step 2: Routing
While still in the VPC Dashboard we'll configure route tables for the subnets. The purpose of routing is to control the traffic flow between the subnets and the internet.
What we'll create:
• A Route Table for the Public Subnet with a route to an Internet Gateway to enable internet access.
• A Route Table for the Private Subnets with routes to a NAT Gateway to ensure they remain isolated from direct internet access.
Let's create the components required for the routing, starting with Internet Gateway
Choose "Internet gateways" from the sidebar and click on Create Internet Gateway.
• Name it "webapp-network-igw" implying that it's the VPC's gateway to the internet
andCreate internet gateway.

Select Attach to VPC from the green pop-up and select the VPC ("webapp-network").

Let's create a NAT Gateway in the same manner.
Before that, we need to allocate an Elastic IP address to the NAT Gateway so that it maintains a consistent public IP address.
Go to "Elastic IPs" and click on Allocate Elastic IP address.
Give it a name tag "ngw-eip" and leave the rest as default.
• Then,Allocate

Now go to "NAT Gateways" and Create NAT Gateway.
• We'll name it "webapp-network-ngw"
• Choose the public subnet we created earlier ("public-subnet") from the dropdown
A NAT Gateway must always be in a public subnet• Choose the Elastic IP (ngw-eip) from the dropdown
• Review andCreate a NAT gateway

Now that we have both Internet Gateway and NAT Gateway created, we can focus on the subject of this step: "Routing"
So let's go to the "Route Tables", from the side menu section to Create Route Table.
• We'll name it "public-rt" and associate it with the VPC ("webapp-network").
• Click Create route tableto confirm.

You'll be redirected to the dashboard of the just created route table
There, select the "Routes" tab. Click on Edit routes and then Add route.
• Set the destination to 0.0.0.0/0 and the target to the Internet Gateway("webapp-network-igw")
Save changes to apply the changes.

Next, we need to associate this route table with the public subnet so that it can use this route table to reach the internet.
• Select the "Subnet associations" tab and click on Edit subnet associations.
• Select the public subnet ("public-subnet") and Save associations

We repeat the process to create a route table for the private subnets.
• We'll name it "private-rt" and associate it with the VPC.
• In the "Routes" tab, add a route with destination 0.0.0.0/0 and target the NAT Gateway ("webapp-network-ngw").
• In the "Subnet associations" tab, associate all three private subnets ("private-subnet-1", "private-subnet-2", and "private-subnet-3") with this route table.


If everything is done correctly the VPC resource map should look like this.
The additional route table was created by default by the lab environment I used. It doesn't affect anything

Key Points / Best Practices
- A NAT Gateway must always be in a public subnet
- Private subnets should not have a direct route to the Internet Gateway, they only interact with the internet for their needs through the NAT Gateway
- Ensure that the route tables are correctly associated with their respective subnets
- Double-check the CIDR blocks to avoid overlaps and ensure proper segmentation
Cloudformation Code for Step 2 - (Routing)
The part of the Cloudformation code that creates the IGW, NAT Gateway and Route Tables as discussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: webapp-network-igw
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
ElasticIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: ngw-eip
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIP.AllocationId
SubnetId: !Ref PublicSubnet
Tags:
- Key: Name
Value: webapp-network-ngw
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: public-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayToInternet
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: private-rt
PrivateRouteToNAT:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
PrivateSubnetRouteTableAssociation3:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet3
RouteTableId: !Ref PrivateRouteTable
Step 3: Security Groups
In this step we will set up Security Groups to control access to the resources we will provision. Security Groups act as virtual firewalls that regulate inbound and outbound traffic. We will create rules to allow only necessary traffic, enhancing the security of our application.
What we'll create: • A Security Group for Bastion Host to allow SSH access from our IP.
• A Security Group for the Web Server to allow inbound HTTP/HTTPS traffic
• A Security Group for the App Server to allow traffic only from the Web Server and Bastion Host Security Groups
• A Security Group for the Database to allow traffic only from the App Server Security and Bastion Host Security Groups

We'll start with the Bastion Host Security Group
• Go to Security in the VPC Dashboard.
• Choose "Security Groups" and click on Create Security Group.
• We'll name it "BastionHostSG"
• Add a description (optional)
• Select VPC (webapp-network)
For Inbound rules:
• Add a rule for SSH access
• Choose "SSH" from the dropdown
• Set the Source to "My IP" and it will auto-fill your current IP address
• Review andCreate security group

On to the Web Server Security Group
• We'll name it "WebServerSG"
• Add 3 inbound rules:
• HTTP - Source: Anywhere IPv4 (0.0.0.0/0)
• HTTPS - Source: Anywhere IPv4 (0.0.0.0/0)
• SSH - Source: "My IP" (for management and testing purposes)
• We should add ICMP rule from App Server SG but we haven't created it yet so we'll come back and add it later
• Review and create the security group


Next, we'll create the App Server Security Group.
• We'll name it "AppServerSG"
• Add 3 inbound rules:
• SSH - Source: Custom - "BastionHostSG" (search "sg" and select BastionHostSG from the dropdown)
• HTTP - Source: Custom - "WebServerSG"
• ICMP - Source: Custom - "WebServerSG"
• We can ingnore https and use only http between web and app server for simplicity
• We should add MySQL/Aurora rule from Database SG but we haven't created it yet so we'll come back and add it later
• Review and create the security group


Finally, we'll create the Database Security Group.
• We'll name it "DatabaseSG"
• Add 2 inbound rules:
• MySQL/Aurora - Source: Custom - "AppServerSG"
• MySQL/Aurora - Source: Custom - "BastionHostSG"
• Review and create the security group

Now, let's go back and add the missing rules to the Web Server and App Server Security Groups.
Select each Security Group and edit the inbound rules
• For WebServerSG, add an ICMP rule with Source: Custom - "AppServerSG"
• For AppServerSG, add a MySQL/Aurora rule with Source: Custom - "DatabaseSG"
• Save the changes
This completes the setup of our Security Groups, ensuring that our application components can communicate securely while minimizing exposure to potential threats.
Key Points / Best Practices
- Allow limited access to specific IPs into bastion host as it has access to both app server and database
- Allowing too much traffic from anywhere can risk your most critical resources
- Since web server is already built to allow traffic from internet, we do not to ssh into it through the bastion host
- Regularly review and update security group rules as needed
- Use descriptive names and comments for rules to make management easier
Cloudformation Code for Step 3 - (Security Groups)
The part of the Cloudformation code that creates the Security Groups as dicussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 and Step 2 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
BastionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable SSH access from my IP
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref MyIP
Tags:
- Key: Name
Value: BastionHostSG
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable HTTP and HTTPS access from anywhere
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref MyIP
Tags:
- Key: Name
Value: WebServerSG
AppServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable access from Web Server and Bastion Host Security Groups
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref WebServerSecurityGroup
- IpProtocol: icmp
FromPort: -1
ToPort: -1
SourceSecurityGroupId: !Ref WebServerSecurityGroup
Tags:
- Key: Name
Value: AppServerSG
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable access from App Server and Bastion Host Security Groups
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref AppServerSecurityGroup
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref BastionSecurityGroup
Tags:
- Key: Name
Value: DatabaseSG
AppServerDatabaseSGIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref AppServerSecurityGroup
IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref DatabaseSecurityGroup
BastionDatabaseSGIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref BastionSecurityGroup
IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref DatabaseSecurityGroup
WebServerAppServerSGIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref WebServerSecurityGroup
IpProtocol: icmp
FromPort: -1
ToPort: -1
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Step 4: EC2 Instances
In this step, we will launch EC2 instances for the bastion server, web server, and app server, configure them with appropriate security groups created in the previous step, and set up user data scripts to automate the installation of necessary software.
What we'll create:
• EC2 Instance for Bastion
• EC2 Instance for Web Server
• EC2 Instance for App Server

• Search for "EC2" in the Search bar and open the EC2 Dashboard.
• Click on "Launch Instance" to create a new EC2 instance.
• We'll create the first instance for the Bastion host.
• Choose an Amazon Machine Image (AMI). Select "Amazon Linux 2023.


• Choose an Instance Type. Select "t2.micro" (eligible for free tier).
• Select a key pair for SSH access. If you don't have one, create a new key pair and download the ".pem" file
you'll need this to connect to your instance later when we test the deployment.

Edit Network Settings:
- Select the VPC (webapp-network)
- Select the public subnet (public-subnet)
- Enable Auto-assign Public IP
- Under Security Group, select "Choose an existing security group" and select the Bastion security group (BastionHostSG)

• Leave everything else as default and scroll to Advanced Details
• In the User Data section, copy and paste the code below:
Bastion Host User Data
set -euxo pipefail
sudo dnf update -y
sudo dnf install -y mariadb105

• Review andLaunch instance
We'll follow the same process to create the remaining 2 instances.
• For the Web Server instance,
-select the public subnet (public-subnet)
-use the WebServerSG security group
-and inside the User data following user data paste this code:
Web Server User Data
set -euxo pipefail
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
• For the App Server instance,
-select the private subnet (public-subnet-1)
-use the AppServerSG security group
-and inside the User data following user data paste this code:
App Server User Data
set -euxo pipefail
sudo dnf update -y
sudo dnf install -y mariadb105
Cloudformation Code for Step 4 - (EC2 Instances)
The part of the Cloudformation code that creates the EC2 Instances as dicussed above
Uploading only this part to Cloudformation will fail to create unless you combine with the codes from the previous steps, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
# ....
# EC2 Instances
BastionHost:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
KeyName: vockey
ImageId: !Ref AL2023AMI
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: 0
SubnetId: !Ref PublicSubnet
GroupSet:
- !Ref BastionSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
sudo dnf update -y
sudo dnf install -y mariadb105
Tags:
- Key: Name
Value: BastionHost
WebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
KeyName: vockey
ImageId: !Ref AL2023AMI
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: 0
SubnetId: !Ref PublicSubnet
GroupSet:
- !Ref WebServerSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
Tags:
- Key: Name
Value: WebServer
AppServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
KeyName: vockey
ImageId: !Ref AL2023AMI
NetworkInterfaces:
- AssociatePublicIpAddress: false
DeviceIndex: 0
SubnetId: !Ref PrivateSubnet1
GroupSet:
- !Ref AppServerSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
sudo dnf update -y
sudo dnf install -y mariadb105
Tags:
- Key: Name
Value: AppServer
Step 5: Database
In this section we are going to create an RDS Database.
• A database is essential for storing and managing the user information that our application will use.
• The App server will connect to this database to perform various operations such as reading, writing, and updating data.
• as well the Bastion host for management purposes as configured in the Security Groups step
Go to the Aurora/RDS Dashboard
• Create first a Subnet Group for the database
• Click on Subnet groups in the left sidebar
• Click on Create DB Subnet Group
• We'll name it "database-subnet-group"
• Provide a description (optional)
• Select the VPC ("webapp-network")

Next, we will add subnets to the group
• Selec the availability zones in which you created the subnets
• Check the 2 private subnets for the Data Tier
-Private-subnet-2 and Private-subnet-3• Click on Create

Now, we can create the database instance
• Go to Databases and click on Create database
• Choose "Standard Create"
• Select "MariaDB" as the engine type

• Leave the engine version as default
• For the template, select "Free tier"
• Set the DB instance identifier to "database-instance"

• Set the Master username to "adminuser"
• Set the Master password and confirm it (make sure to note this down somewhere)

• Move to connectivity section
• Select the VPC ("webapp-network")
• Select the subnet group created ("database-subnet-group")

• Set Public access to "No"
• Set VPC security group to "Choose existing" and select "DatabaseSG"

• Skip to Additional configuration expand it
• Set Initial database name to "db" for simplicity
- This will create a database named "db" on the database-instance• Since this architecture isn't fully functional, we will uncheck the backup and monitoring options for faster database creation
• Review and Create database

• It will take a few minutes for the database to be created
• Once available, note down the endpoint, close to where you kept the Master password, we will need it to test connections to the database through the appserver in the next step
Essentially, to establish a connection to the database you will require the following
- Endpoint
- Database name (in our case "db")
- Master username (in our case "adminuser")
- Master password (the one you set)

Cloudformation Code for Step 5 - (Database)
The part of the Cloudformation code that creates the Database as dicussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 through Step 4 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
# ....
# EC2 Instances
# ....
# Database
DatabaseSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS instance
SubnetIds:
- !Ref PrivateSubnet2
- !Ref PrivateSubnet3
Tags:
- Key: Name
Value: database-subnet-group
DatabaseInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: database-instance
AllocatedStorage: 20
DBInstanceClass: db.t4g.micro
Engine: mariadb
EngineVersion: "11.4.5"
MasterUsername: !Ref DBMasterUsername
MasterUserPassword: !Ref DBPassword
VPCSecurityGroups:
- !Ref DatabaseSecurityGroup
DBSubnetGroupName: !Ref DatabaseSubnetGroup
MultiAZ: false
PubliclyAccessible: false
StorageType: gp2
DBName: db
DeletionProtection: false
BackupRetentionPeriod: 0
MonitoringInterval: 0
EnablePerformanceInsights: false
AutoMinorVersionUpgrade: false
CopyTagsToSnapshot: false
Tags:
- Key: Name
Value: database-instance
Step 6: Testing
In this final step, we will test the entire setup to ensure that all components are functioning correctly. We will verify that the App server can communicate with the database, as well with the Web Sever. We will also test if the Web server is serving the web application properly.
We will test if only permitted access is allowed to all the Instances.
We will need a terminal for this step. I will be using WSL terminal on Windows.
First download this html file and save it to your home directory(or the directory you will be using the terminal in). This file will be used to test if the web server is serving the web application properly.
📄Download webapp-index.htmlAnd then upload they key pair pem file you created in Step 4 to the same directory.

Rename the file to index.html
The file kept interfering with other index.html files while developing the project that's the reason I named it webapp-index.html but you must rename it to index.html
Run this command:
mv webapp-index.html index.html

Make sure your key pair file has the right permissions set.
Run this to do so:
If prompted to confirm key authenticity type "yes" and hit enter.

Follow same procedure to upload the key pair pem file to the Bastion Host instance.
To use it to connect to the App server instance from the Bastion Host.
Run the command:
scp -i YOUR-KEY-PAIR.pem YOUR-KEY-PAIR.pem ec2-user@YOUR-BASTION-HOST-PUBLIC-IP:~/

Let's ssh into the Web server and copy the html file to /var/www/html/ so that it can serve the webpage
ssh -i YOUR-KEY-PAIR.pem ec2-user@YOUR-WEB-SERVER-PUBLIC-IP
You can see the index.html file in the home directory by running "ls"

Now open a browser and run http://YOUR-WEB-SERVER-PUBLIC-IP/ and you see this page below
Note that if you run https insted of http it might not work

Now let's test if the App server can communicate with the Database
Exit the web server instance by running "exit" command
And then ssh into the Bastion Host instance
ssh -i YOUR-KEY-PAIR.pem ec2-user@YOUR-BASTION-HOST-PUBLIC-IP
Now we are in the Bastion Host instance
"ls" to see if the key pair file we uploaded is there

From here we will ssh into the App server instance
ssh -i YOUR-KEY-PAIR.pem ec2-user@YOUR-APP-SERVER-PRIVATE-IP
Now we are in the App server instance

Run this command to test if the App server can communicate with the Database server
mysql -h YOUR-DATABASE-ENDPOINT -u MASTER-USERNAME -p DATABASE-NAME
Go to the RDS Dashboard and copy the Database Endpoint value if you didn't note it somewhere earlier
Enter the master password for the database at the prompt
If you see the MariaDB prompt it means the App server can communicate with the Database server

You can run further tests on your own to verify if the security groups are working as intended
For example try to ssh into the App server instance from your local machine
It should not work because the security group of the App server only allows ssh access from the Bastion Host
You can ping the Web Server's private IP from the App Server to check if the ICMP rule works fine
Voila! Congratulations!
That's all about it, we've been able to successfully complete the design
Thank You
Thank you for following this guide on building a 3-tier web application on AWS. I hope you found it informative and helpful. If you have any questions or need further assistance, feel free to reach out!
Get In Touch
Have questions about this project or want to collaborate? I'd love to hear from you!

Dickson Ankamah
Cloud Practitioner
I'm always open to discussing new opportunities, interesting projects, or just having a chat about technology. Feel free to reach out through any of the channels above!