Project by: Dickson Ankamah

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

Topology Diagram
Topology Diagram

Prerequisites

  • • AWS Account with appropriate IAM users and permissions
  • • Familiarity with AWS Management Console
  • • A Command Line Interface (CLI) tool
  • I will use a wsl terminal in this guide

  • • Familiarity with Linux Commands
  • • Familiarity with AWS CloudFormation (optional but recommended)

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

Container setup
Container setup

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.

Container setup

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

Subnet create screenshot
Subnet create screenshot

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.

Internet gateway create

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

Internet gateway attach

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

Elastic IP create

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

NAT Gateway create

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.

Public route table create

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.

Public route table route

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

Public route table association

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.

Private route table route
Private route table association

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

VPC resource map

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

Bastion Host SG screenshot

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

Web Server SG screenshot
Web Server SG screenshot

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

App Server SG screenshot
App Server SG screenshot

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

Database SG screenshot

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

EC2 and Database Setup Diagram

• 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.

EC2 Dashboard
EC2 AMI Selection

• 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.

EC2 Instance Type Selection

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)

EC2 Network Settings

• 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

# Copy this and paste it into the user data
#!/bin/bash
set -euxo pipefail
sudo dnf update -y
sudo dnf install -y mariadb105
EC2 User Data

• 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

# Copy this and paste it into the user data
#!/bin/bash
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

# Copy this and paste it into the user data
#!/bin/bash
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")

RDS Subnet Group screenshot

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

RDS Subnet Group screenshot

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

RDS Engine screenshot

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

RDS Instance ID screenshot

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

RDS Master User screenshot

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

RDS Connectivity screenshot

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

RDS Connectivity screenshot

• 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

RDS Additional Configuration screenshot

• 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)

RDS Endpoint screenshot

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.html
And then upload they key pair pem file you created in Step 4 to the same directory.

Uploading the key pair and html file to the terminal 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

Renaming the html file to index.html

Make sure your key pair file has the right permissions set.

Run this to do so:

chmod 400 YOUR-KEY-PAIR.pem
Now run the following command to upload the html file to the web server instance.
scp -i YOUR-KEY-PAIR.pem index.html ec2-user@YOUR-WEB-SERVER-PUBLIC-IP:~/
Go to your web server instance in the EC2 Dashboard and copy the Public IP.
If prompted to confirm key authenticity type "yes" and hit enter.
Upload the index.html file to the web server

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:~/

Upload the key pair pem file to the Bastion Host

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"

SSH into the web server instance
This command to copies the file:
sudo cp index.html /var/www/html/


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

Webpage served by the web server

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

SSH into the Bastion Host instance

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

SSH into the App server instance from the Bastion Host

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

MySQL prompt on the App 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
Topology Diagram

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

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!