Building and Deploying a 3-tier Architecture with AWS CloudFormation

Nife Sofowoke
11 min readApr 30, 2024

--

In this project, I built and deployed a 3-tier architecture consisting of a Web, Application, and Database tier for a fictional e-commerce company, "SwiftCart”, using an AWS CloudFormation template to provide a scalable and reliable platform that could handle a large number of users and transactions.

What is a 3-tier Architecture?

The Three-tier architecture is a software application architecture that organizes applications into three logical and physical computing tiers: the web tier, also known as the user interface, the presentation tier or front end, the application tier also known as the business logic tier or middle tier, and finally the database tier also known as the back-end tier.

Web tier (User Interface): This tier’s main purpose is to provide a user-friendly and visually appealing interface for user interaction and data presentation to users by serving this content to the browser in the form of HTML/JavaScript/CSS.

Application tier (Business Logic tier): The application tier is like the heart of the application. It contains the application logic and business rules that define how data collected from the web tier is processed and manipulated. This tier also interacts with the database layer using API calls.

Database tier (Back-end): This tier is where all the data used by the application is stored and managed. It is responsible for data retrieval, storage, and manipulation. It includes databases, file systems, or any other data storage mechanisms.

The separation of each tier allows for better resource utilization and facilitates the reuse of components across different parts of the application. Additionally, it enables easier integration with other systems and technologies.

Use Case

A small e-commerce company wants a scalable and reliable e-commerce platform and decides to deploy a 3-tier architecture at the advice of their engineers.

  • The web tier will be responsible for serving the static content to end-users and handling incoming requests.
  • The application tier will be responsible for processing transactions, handling payments, and interacting with the database.
  • The database tier will store all of the platform’s data, including customer information, product data, and transaction records.

Prerequisites

  1. An AWS account. You can sign up for free here.
  2. A source code editor to build and edit your code. I used Visual Studio Code.

Implementation Steps

For this project, I used a YAML code template for Cloudformation.

For the web tier, the resources deployed using the Cloudformation template include the following:

  • a VPC
  • 2 public subnets
  • a public route table associated with the two public subnets
  • an internet gateway attached to the VPC
  • an elastic IP
  • a NAT gateway in one of the public subnets to allow instances in private subnets
  • a launch template with a bootstrap script to install and launch Apache and create a custom web page
  • an autoscaling group with a desired capacity of two EC2 instances
  • a scaling policy
  • EC2 web server security group allowing incoming HTTP traffic from any source IP address (0.0.0.0/0)
  • a bastion host server created in the web tier to securely access the EC2 instances in the application tier.
  • a bastion host security group in the public subnet to reach EC2 instances in the private subnet
  • an application load balancer

If you would like a more detailed deployment of a web tier using cloud formation, you can read a previous blog I created about it here.

For the application tier, the resources deployed include:

  • 2 private subnets
  • a private route table
  • an autoscaling group with a desired capacity of two EC2 instances
  • an EC2 web server security group allowing inbound permissions from the bastion host security group

For the Database tier, the resources deployed include:

  • 2 private subnets
  • an RDS subnet group
  • an RDS instance security group
  • a MySQL RDS database

I used the Cloudformation template below to deploy all the resources needed for the 3-tier-architecture

AWSTemplateFormatVersion: '2010-09-09'

Description: This template creates a Web, Application & Database Tier for a fictional e-commerce company "SwiftCart"

Parameters:
MasterUsername:
Type: String
Description: The username for the database.

MasterUserPassword:
Type: String
Description: The password for the database.
NoEcho: true

MyIPAddress:
Type: String
Description: My IP address for the bastion host.

KeyPair:
Type: String
Description: SSH Key for the bastion host & EC2 instances.

Resources:
# Web Tier
# VPC
SwiftCartVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.10.0.0/16
EnableDnsSupport: 'true'
EnableDnsHostnames: 'true'

# Security Group for Web Tier
SwiftCartSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP & SSH access
VpcId:
Ref: SwiftCartVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
CidrIp: 0.0.0.0/0
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 10.10.0.0/16

# Internet Gateway
SwiftCartInternetGateway:
Type: AWS::EC2::InternetGateway

# Attach IG to VPC
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: SwiftCartVPC
InternetGatewayId:
Ref: SwiftCartInternetGateway

# Public Subnets for Web Tier
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: SwiftCartVPC
CidrBlock: 10.10.1.0/24
AvailabilityZone: "us-east-1a"
MapPublicIpOnLaunch: true

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: SwiftCartVPC
CidrBlock: 10.10.2.0/24
AvailabilityZone: "us-east-1b"
MapPublicIpOnLaunch: true

# Public Route Table
SwiftCartPublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: SwiftCartVPC

# Configuring Public Route Table
SwiftCartRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: SwiftCartPublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: SwiftCartInternetGateway

# Route association for Public Subnets
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PublicSubnet1
RouteTableId:
Ref: SwiftCartPublicRouteTable

PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PublicSubnet2
RouteTableId:
Ref: SwiftCartPublicRouteTable

# NAT Elastic IP
SwiftCartElasticIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc

# NAT Gateway for Web Tier
SwiftCartNatGateway:
Type: AWS::EC2::NatGateway
DependsOn: PublicSubnet1
Properties:
ConnectivityType: public
AllocationId: !GetAtt SwiftCartElasticIP.AllocationId
SubnetId: !GetAtt PublicSubnet1.SubnetId

# Bastion Host
SwiftCartBastionHost:
Type: AWS::EC2::Instance
Properties:
SecurityGroupIds:
- !Ref SwiftCartBastionHostSG
ImageId: ami-0f403e3180720dd7e
InstanceType: t2.micro
SubnetId: !Ref PublicSubnet1
KeyName: !Ref KeyPair

# Bastion Host Security Group
SwiftCartBastionHostSG:
Type: AWS::EC2::SecurityGroup
DependsOn: SwiftCartVPC
Properties:
GroupName: !Sub SwiftCartBastionHostSG
GroupDescription: "Security group for Bastion Host"
VpcId:
Ref: SwiftCartVPC

# Bastion Host Security Group SSH Ingress Rule
SwiftCartBastionHostSGIngress:
Type: AWS::EC2::SecurityGroupIngress
DependsOn: SwiftCartBastionHostSG
Properties:
CidrIp: !Ref MyIPAddress
Description: "Allow SSH from my local machine"
GroupId:
Ref: SwiftCartBastionHostSG
IpProtocol: tcp
FromPort: 22
ToPort: 22

# Launch Template for Web & App Tier
SwiftCartLaunchTemplate:
Type: 'AWS::EC2::LaunchTemplate'
Properties:
LaunchTemplateName: SwiftCartLaunchTemplate
LaunchTemplateData:
KeyName: !Ref KeyPair
InstanceType: t2.micro
ImageId: ami-0f403e3180720dd7e
NetworkInterfaces:
- DeviceIndex: 0
AssociatePublicIpAddress: true
DeleteOnTermination: true
Groups:
- !Ref SwiftCartSG
UserData:
Fn::Base64: !Sub |
#!/bin/bash
yum update -y
yum install httpd -y
systemctl start httpd
systemctl enable httpd
amazon-linux-extras install epel -y
yum install stress -y
echo "<h1>Welcome To Swift Cart </h1>" > /var/www/html/index.html

# Auto Scaling Group for Web Tier
SwiftCartASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref SwiftCartLaunchTemplate
Version: !GetAtt SwiftCartLaunchTemplate.LatestVersionNumber
MaxSize: '5'
MinSize: '2'
DesiredCapacity: '2'
VPCZoneIdentifier:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
MetricsCollection:
- Granularity: 1Minute

# Scaling Policy for Web Tier
SwiftCartScalingPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref SwiftCartASG
AdjustmentType: ChangeInCapacity
ScalingAdjustment: '1'

# Application Load Balancer
SwiftCartApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: "SwiftCartALB"
SecurityGroups:
- !Ref SwiftCartSG
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2

# ALB Target Group
SwiftCartALBTargetGroup:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
HealthCheckIntervalSeconds: '30'
HealthCheckTimeoutSeconds: '5'
Port: '80'
Protocol: HTTP
VpcId: !Ref SwiftCartVPC

# ALB Listener
LUBankALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref SwiftCartALBTargetGroup
LoadBalancerArn: !Ref SwiftCartApplicationLoadBalancer
Port: 80
Protocol: HTTP

# Application Tier
# Private Subnets for Application Tier
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SwiftCartVPC
CidrBlock: 10.10.3.0/24
AvailabilityZone: "us-east-1a"
MapPublicIpOnLaunch: false

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SwiftCartVPC
CidrBlock: 10.10.4.0/24
AvailabilityZone: "us-east-1b"
MapPublicIpOnLaunch: false

# Private Route Table for Application Tier
SwiftCartPrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref SwiftCartVPC

# Configuring Private Route Table for Application Tier
SwiftCartPrivateRoute:
Type: AWS::EC2::Route
DependsOn: SwiftCartNatGateway
Properties:
RouteTableId: !Ref SwiftCartPrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref SwiftCartNatGateway

# Route association for Private Subnets in Application Tier
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref SwiftCartPrivateRouteTable

PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref SwiftCartPrivateRouteTable

# Security Group for Application Tier
SwiftCartSG2:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP & SSH access
VpcId: !Ref SwiftCartVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref SwiftCartBastionHostSG

# Auto Scaling Group for Application Tier
SwiftCartApplicationASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref SwiftCartLaunchTemplate
Version: !GetAtt SwiftCartLaunchTemplate.LatestVersionNumber
MaxSize: '5'
MinSize: '2'
DesiredCapacity: '2'
VPCZoneIdentifier:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
MetricsCollection:
- Granularity: 1Minute

# Scaling Policy for Application Tier
SwiftCartPrivateScalingPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref SwiftCartApplicationASG
AdjustmentType: ChangeInCapacity
ScalingAdjustment: '1'

# Database Tier
# Database Instance Security Group
SwiftCartDBInstanceSG:
Type: AWS::RDS::DBSecurityGroup
Properties:
GroupDescription: Ingress for Amazon EC2 security group
EC2VpcId: !Ref SwiftCartVPC
DBSecurityGroupIngress:
- EC2SecurityGroupId: !Ref SwiftCartSG

# RDS Private Subnets
RDSPrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SwiftCartVPC
CidrBlock: 10.10.5.0/24
AvailabilityZone: "us-east-1a"
MapPublicIpOnLaunch: false

RDSPrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SwiftCartVPC
CidrBlock: 10.10.6.0/24
AvailabilityZone: "us-east-1b"
MapPublicIpOnLaunch: false

# RDS Subnet Group
SwiftCartSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: SwiftCart subnet group
DBSubnetGroupName: SwiftCartSubnetGroup
SubnetIds:
- !Ref RDSPrivateSubnet1
- !Ref RDSPrivateSubnet2

# Route association for RDS Private Subnets
RDSPrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref RDSPrivateSubnet1
RouteTableId: !Ref SwiftCartPrivateRouteTable

RDSPrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref RDSPrivateSubnet2
RouteTableId: !Ref SwiftCartPrivateRouteTable

# RDS DB Instance
SwiftCartDBInstance:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 15
DBInstanceIdentifier: SwiftCart
Engine: MySQL
EngineVersion: '8.0.35'
DBSecurityGroups:
- !Ref SwiftCartDBInstanceSG
DBSubnetGroupName: !Ref SwiftCartSubnetGroup
MasterUsername: !Ref MasterUsername
MasterUserPassword: !Ref MasterUserPassword
DBInstanceClass: db.m6gd.large
MultiAZ: false

Stack Creation

  1. In the AWS console, navigate to Cloudformation.
  2. Choose to create a stack
  3. Upload the .yaml template file
  4. Specify any stack details like ‘stack name’, ‘database name’, passwords, and so on
  5. Create stack

It takes a few minutes for resources to be created.

successful stack creation

Verification of EC2 Instances and Other Resources Created

  1. Verified the VPC, 2 public subnets, 4 private subnets, 3 route tables, internet gateway, and NAT gateway were created.
VPC, subnets, route tables, NAT gateway, and IGW in the VPC resource map

2. Verified the application load balancer was created.

ALB

3. Verified the web and application tiers auto-scaling groups were created.

ASGs

4. Verified the creation of four instances (2 each for the web and application tiers).

Connecting to Web Servers

Verified I could access the front end of the web tier from the internet by retrieving the DNS URL of the Application Load Balancer and testing it in a web browser.

SwiftCart custom website

Connecting to the Bastion Host & Application Tier

The last steps of verification I did to make sure the 3 tiers were properly integrated and were working as they were designed to include:

  • connecting to my Bastion Host web server through SSH
  • pinging the application tier.
  • ‘SSHing’ into one of the instances in the Application tier fro my Bastion Host

Quick Note on SSH Agents

Before connecting to the Bastion Host using SSH, I had to set up an SSH agent for SSH agent forwarding. An SSH agent stores your private key pairs and certificates in memory, ready to use for authentication by SSH. Agent forwarding on the other hand, allows your local SSH agent to get through an existing SSH connection and transparently authenticate on a more distant server. Therefore, because I already have an existing SSH connection (bastion host), I need to use agent forwarding to SSH once more into my Application server using the bastion host.

  1. I used the command below to add my Bastion Host Key pair to an SSH agent on my local machine
ssh-add private-key.pem

2. I verified that the key I added was available to the SSH agent using the command below:

ssh-add -L

3. I SSHed into my Bastion Host using the command below:

  • the -A option enables agent forwarding
  • the -v option enables SSH to work in verbose mode
ssh -v –A ec2-user@publicIPv4
command used to SSH into bastion host
successful SSH into bastion host

4. Successfully pinged the application tier from the web tier using the bastion host. I had to ensure that the networking setup was operational and that data could be transmitted between the two tiers as needed. To do this, I use the ping command + the private IP address of an instance in the application tier.

succeeful ping

The application tier instance received the ping requests and responded with ping replies, and the web tier instance received the ping replies from the application tier instance. This successful ping signifies that there is a functional network path between the web tier and application tier instances, allowing them to communicate with each other.

5. Successfully SSHed into one of the private instances in the application tier from the bastion host using the command below:

ssh -v ec2-user@privateIPv4
command used to connect to application tier
successful connection to the application tier

Notice how I did not provide a key pair name in both SSH commands above. This is because I used an SSH agent.

With the success of my verification steps above, I confirmed that the resources needed for each tier were successfully deployed and functioning how they were supposed to, and also cohesive as a whole.

Results

  1. Designed and deployed a highly available, scalable, and reliable 3-tier architecture using Infrastructure as Code.
  2. Practiced resource orchestration and dependency management through knowledge of how different resources, e.g, VPC, instances, subnets, etc are interconnected and designed with this in mind.
  3. Incorporated automation using CloudFormation during the creation of the 3-tiers.

Bonus: Lessons Learned

An important lesson I learned during the process of designing and deploying my 3-tier architecture was to ‘test as I build’. After being a victim of countless code errors (that could’ve been easily avoided) and hoursss of debugging, I can’t stress how important this is. It not only helps you catch bugs early, it reduces the time and resources spent on troubleshooting, validates that your code meets the specific requirements, and verifies that each component works correctly and produces the desired output.

Clean Up

To prevent incurring costs from the resources deployed, delete the CloudFormation stack created. This will delete all associated resources.

Thanks for reading! I hope you found this project valuable.

Feel free to connect with me on LinkedIn or leave any constructive feedback you have in the comments.

--

--

Nife Sofowoke

A tech enthusiast on an exciting journey of transitioning into the field of Cloud/Devops Engineering.