A Practical Guide To Deploying A Three-tier Application On AWS
Tags: aws, devops, cloud, terraform
Published: 2023-06-01 09:26:55

This post was originally published at [Practical Cloud](https://practicalcloud.net).

View the longer, original version [here](https://practicalcloud.net/a-practical-guide-to-deploying-a-complex-production-level-three-tier-architecture-on-aws/).

**Multi-tiered architectures** have become the most popular ways of designing and building applications in the cloud. This is primarily because they offer high availability, replication, flexibility, security and many other numerous benefits. This is as opposed to single-tier architectures which typically involves packaging all the requisite components of a software application into a single server. 

The most popular multi-tier design pattern is a three-tier architecture. The three-tier architecture consists of the following layers:

- **Presentation Layer:**  This is the outermost layer of the application and provides an interface for interacting with the user. It also provides a secure communication channel with the other tiers.
- **Logic Layer:** This is where information is processed and application logic is executed. It processes input from the presentation layers, processes it and also communicates with the data layer when necessary. it is also known as the _application layer_ or _middleware_.
- **Data Layer:** This is where the database management system sits, thus providing it with a secure, isolated environment for storing and managing application information.

In this guide, we are going to design a very fault-tolerant, highly scalable Flask application on AWS using the three-tier architecture design pattern.

## Architectural Design
The Presentation layer will comprise of; Cloudfront, Elastic Load Balancer, Internet Gateway, NAT Gateway and two Bastion Hosts.
The Application (Logic) Layer will consist of EC2 instances based on EBS volumes, provisioned through an Auto Scaling group.
The Data Layer will be made up of a PostgresQL Database with a Read Replica. The EC2 instances provisioned in the application layer will be connected to an Elastic Filesystem for data storage. So technically, the EFS is also residing in this layer.

**Bonus:** 
- [AWS Route 53](https://aws.amazon.com/route53/) will be used to provide a domain name for the application.
- We will integrate [ClodFront](https://aws.amazon.com/cloudfront/) with the Application Load Balancer to provide worldwide accelerated content delivery and reduce latency.
- [Terraform](https://github.com/Kelvinskell/terra-tier/tree/master/terraform) will be used as the Infrastructure-as-code (IAC) tool to automate this whole process from end to end.

**To automatically create this architecture from end to end, click on this [Link](https://github.com/Kelvinskell/terra-tier) to get the Terraform code.**

This is a first-part of a three series project. 
In the [second project](https://dev.to/kelvinskell/devops-on-aws-codepipeline-in-action-60k), we will implement **DevOps on AWS** via **CodePipeline**, **CodeBuild** and **CodeDeploy**. 
The third project introduces a **Reporting Layer** into the architecture.
This part focuses soley on designing and deploying three-tier architecture for a Flask application on AWS.

Let's begin. Shall we?
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bncci0sp1k4j8fthe4sz.jpg)

## Step 1: Create The Environment.
Firstly we will create a [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html). A virtual private cloud is a secure, isolated, networking environment hosted in a public Cloud. A VPC is more or less a virtual data center in the cloud. This is where most of our application resources will be hosted.



![AWS VPC environment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4mllajrcqc45cnxto9d.png)
My VPC has a name tag of project-x. This means that all other resources created within this VPC will have the prefix of "project-x".
You can also notice that I am assigning a CIDR block of 10.0.0.0/16 to my VPC. This is a block of private IP addresses from which my VPC resources will get their local  addresses.


![AWS VPC Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5eruqo01r3p879e8alq7.png)
Notice that I am in the us-east-1 region (North Virginia).
Our VPC will be provisioned in three availability zones. 6 subnets will be created in total. Three public and three private ones. The public subnets are public soley because an internet gateway will be attached to them.
Also notice that I am creating a NAT gateway. This NAT gateway will be used by the EC2 instances hosting our application to connect to the internet (E.g: For Updates).

## Step 2: Create A Security Group
A security group acts like a firewall and determines how traffic will enter or exit instances.
Since our web servers will be hosted in the logic-tier, it is important that we restrict the type of traffic entering them.

The name of our security group will be "project-x-logic-tier-sg"
The security group must allow inbound traffic on **port 5000** since that is the port on which Flask listens on.

![Creating a security group](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqr2lx9xj0djpf6njnp1.png)



## Step 3: Create A Launch Template

A [Launch Template](https://docs.aws.amazon.com/autoscaling/ec2/userguide/launch-templates.html) is a way of specifying important configuration details such that we can then launch instances using the details in the template. This template will be used by an auto scaling group to create instances in our Application tier.

![AWS EC2 Management Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ucdf24p3vo4s204l015j.png)
I assigned a name and description to my template. Notice also that i duly tagged the environment as "prod".

![Ec2 Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p18jo7e5gkliyeefk8dj.png)
I am using a "t3.xlarge" instance type. 
Be sure to select the "project-x-logic-tier-sg" security group.
Leave every other detting as is.
Under **"Advanced details"**, I am going to paste in the following under user data:

```
#!/bin/bash

# Mount EFS
fsname=fs-093de1afae7166759.efs.us-east-1.amazonaws.com # You must change this value to represent your EFS DNS name.
mkdir /efs
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport $fsname:/ /efs

# install and set up Flask
apt update
apt upgrade -y
apt install python3-flask mysql-client mysql-server python3-pip python3-venv -y
apt install sox ffmpeg libcairo2 libcairo2-dev -y
apt install python3-dev default-libmysqlclient-dev build-essential -y

```

## Step 4: Create an Autoscaling Group, Elastic Load balancer And Target Group

The Load balancer is the entry point to the application.
The [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html), residing in the **presentation layer**, will route traffic through the [AutoScaling Group](https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-asg-launch-template.html) to logic-tier instances residing in the **logic layer**.


![EC2 Management Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/35d7als2rz8zq167gx1u.png)


![Creating an autoscaling group](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/29aswslro3px62b07nrq.png)
I am naming this Autoscaling group "project-x-asg"
Click on **next**. Select the "**project-x-vpc**" we created earlier. Also make sure that you select only the private subnets from the three availability zones. This is very crucial since our VPC will be launched in the logic tier. The subnets must not be public.

![Creating an ASG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcokqlzna6ppfx8lg759.png)

Click on **next**.
Under "**Configure advanced options**", select "**Attach to a new load balancer**". Also select "Create a target group".
On the next page, we'll configure our ASG to have a minium capacity of 2, a desired capacity of 4 and a maximum capacity of 6. We'll also set up scaling based on target tracking metrics to scale based on average CPU Utilization.


![Create an ASG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dsqypntd2n2gw4d860w6.png)
You can decide to add tags. I'll add a tag name of "Environment" with a value of "Prod". this will be needed during the CodeDeploy stage.

![creating an ASG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72ppnknmsh4ewz3b8klg.png)

**Create your ASG.**

## Step 5: Attach An Elastic Filesystem
[AWS EFS](https://aws.amazon.com/efs/) is a fully managed, highly scalable shared storage solution in the cloud. It is NFS compatible.
This Elastic filesystem will provide shared storage for all our application tier servers. Since it provides storage, EFS sits in the **data layer** of the three tier architecture.

First **Create a new security group**. This security group should allow only inbound NFS traffic from the security group of our logic-tier instances. You can get a detailed guide on how to create that **[Here](https://catalog.workshops.aws/general-immersionday/en-US/basic-modules/60-s3/efs/3-efs#:~:text=Creating%20a%20Security%20Group%20for%20EFS&text=Click%20on%20Create%20Security%20Groups%20in%20the%20top%20right%20of%20the%20screen.&text=EC2%20to%20EFS-,VPC%20%2D%20This%20is%20important.,created%20in%20an%20earlier%20step.)
**
Go to the EFS console. Click on "**Create filesystem**" and then click on **customize**.

![EFS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hg99g4m4sgy6uqhmd6iu.png)

Assign a name to your EFS. Leave every other setting as default. Click on **Next**.

![EFS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1zvianh79qt6hm83huey.png)
On the **Network access page**, select your _propject-x_ VPC. Select the EFS security group you have created. Click on next.

Under **Filesystem Policy**, leave everything as default. 
![EFS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je6e5qmq6jt2c2aeydqu.png)

Skip to **Review** and then Create your filesystem.
Now we must update our user data to look like this:

```
#!/bin/bash

# Use Google's DNS
echo "nameserver 8.8.8.8" >> /etc/resolv.conf

# Force apt to use IPV4
apt-get -o Acquire::ForceIPv4=true update

# Change hostname
echo "project-x-app-server" > /etc/hostname

# Install efs-utils
apt-get install awscli -y
mkdir /efs
sudo apt-get -y install git binutils
git clone https://github.com/aws/efs-utils
cd /efs-utils
./build-deb.sh
apt-get -y install ./build/amazon-efs-utils*deb

# Mount EFS
fsname=$(aws efs describe-file-systems --region us-east-1 --creation-token project-x --output table |grep FileSystemId |awk '{print $(NF-1)}')
mount -t efs $fsname /efs

# Get DB credentials
DB=$(aws rds describe-db-instances --db-instance-identifier --region us-east-1 database-1 --output table |grep DBName |awk '{print $(NF-1)}')
HOST=$( aws rds describe-db-instances --db-instance-identifier --region us-east-1 database-1 --output table |grep Address |awk '{print $(NF-1)}')
ARN=$(aws secretsmanager list-secrets --region us-east-1 --filters "Key=tag-value, Values=project-x-rds-mysqldb-instance" --output table |grep ARN |awk '{print $(NF-1)}')
USER=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id $ARN --output table |grep -w SecretString |awk '{print $3}' |cut -d: -f2 |sed 's/password//' |tr -d '",')
PRE_PASSWORD=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id $ARN --output table |grep -w SecretString |awk '{print $3}' |cut -d: -f3 |tr -d '"')
PASSWORD=${PRE_PASSWORD%?}

# install and set up Flask
apt-get update -y && apt-get upgrade -y 
apt-get install python3-flask mysql-client mysql-server python3-pip python3-venv -y 
apt-get install sox ffmpeg libcairo2 libcairo2-dev -y 
apt-get install python3-dev default-libmysqlclient-dev build-essential -y 

# Clone the app
cd /
git clone https://github.com/Kelvinskell/terra-tier.git
cd /terra-tier

# Populate App with environmental variables
echo "MYSQL_ROOT_PASSWORD=$PASSWORD" > .env
cd /terra-tier/application
echo "MYSQL_DB=$DB" > .env
echo "MYSQL_HOST=$HOST" >> .env
echo "MYSQL_USER=$USER" >> .env
echo "DATABASE_PASSWORD=$PASSWORD" >> .env
echo "MYSQL_ROOT_PASSWORD=$PASSWORD" >> .env
echo "SECRET_KEY=08dae760c2488d8a0dca1bfb" >> .env # FLASK EXTENSION KEY. NOT NECESSARILY A "SECRET".
echo "API_KEY=f39307bb61fb31ea2c458479762b9acc" >> .env 
# YOU TYPICALLY DON'T ADD SECRETS SUCH AS API KEYS AS PART OF SOURCE CONTROL IN PLAIN TEXT.
# THIS IS BEIGN ADDED HERE SO THAT YOU CAN EASILY REPLICATE THIS INFRASTRUCTURE WITHOUT ANY HASSLES.
# YOU CAN REPLACE IT WITH YOUR OWN MEDIASTACK API KEY.

# Setup virtual environment
cd /terra-tier
python3 -m venv venv
source venv/bin/activate

# Run Flask Application
pip install -r requirements.txt
export FLASK_APP=run.py
export FLASK_ENV=production
flask run -h 0.0.0.0
```

Here is where you find your filesystem's DNS name.
![AWS EFs Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqcuscdmjsacmli29j1f.png)
Make sure to change the value of the "**fsname**" variable to represent your own filesystem's DNS name. 


## Step 6: Create A Bastion Host
A [Bastion Host](https://dev.to/aws-builders/bastion-host-in-aws-vpc-2i63) is a special server used to manage access to servers sitting in an internal network or other private AWS resources from an external network. The bastion host sits in the public network and provides limited access to administrators to log in to servers sitting in an isolated network. It is also commonly referred to as a Jump Box or Jump Server.

From the bastion host, we'll be able to gain SSH access into our application layer servers, for administrative purposes.

**[Here](https://www.strongdm.com/blog/bastion-hosts-with-audit-logging-part-one)** is a good resource on how to create your bastion host and connect to your logic layer servers through it.
**Security Caution:** Connecting to bastion hosts using ssh key-pairs is no longer the recommended practice. Instead use [AWS Systems Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html) for a more secure connection and tunneling through the bastion host to your private instances.

## Step 7: Create A Database
The database solidly sits in the data layer, together with the Elastic filesystem.
Our Flask application will need to connect to a relational database. We will be using MYSQL for this architecure.
[MYSQL](https://dev.mysql.com/doc/refman/8.0/en/what-is-mysql.html) is a widely relational database management system (DBMS) which is free and open source. MYSQL is renowned for its ability to support large production workloads, hence its suitability for our project. 

We will however be utilizing [Amazon RDS for MYSQL](https://aws.amazon.com/rds/mysql/). It is a fully managed database solution in the cloud.

On the RDS Console, click on **Databases** and then clcik on "**Create database**".

![RDS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ki17yavayflw9c3bl08.png)
I am selecting a **Production** template and choosing **Multi-AZ DB-instance**.
![AWS RDS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cal54wz82c3lj89puln7.png)

Select your master username and DB Instance Class. We'll be using AWS SEcrets manager for managing our DB credentials.

![Creating RDS database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/33ollcketa928eyz991l.png)


Under "**Connectivity**", select the **project-x-vpc**. Click on "**Create a new security group**".
![Create MYSQL RDS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jylinnm1l23xldmzz4c.png)

Create a security group that **only** allows incoming traffic on **port 3306** from security group of the application layer servers. Select the security group.

Under "**Database Authentication**", choose "**password**".

Go down to "**Advanced Options**", Under "**database name**", choose "**newsreadb**". (You must choose this exact name for the Flask App to be able to connect to the database). 

Click on **Create database**.

**Now our architecture is almost ready !!!**
![AWS 3-tier Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8zzrfv4uui3qmzd8591.png)

To connect to the database, our application will execute API calls to AWS Secrets Manager in order to get the DB credentials. Therefore, we must create an [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) role that allows it to perform that. We will also modify the launch template to attach the role as instance profile for our application layer servers.


## Step 8: Create An IAM Role And Modify Launch Template

Go to the IAM Dashboard and click on **Roles**.
Click on **Create Role**. Under **UseCase**, select EC2 and click **Next**.
Search for secrets and select the permission that pops up.

![AWS IAM Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5ws6a8f0pt35x2uszk2.png)

Click on **next** and click on **Create Role**.

Next, we'll need to modify our launch template to include this role so that our logic tier servers can assume it to communicate with AWS Secrets Manager.


Go to the EC2 Console, select launch templates, Actions and click on "**Modify launch template**"

![Modify launch template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcuvfof08vgbgxcgl0lb.png)

Go to "**Advanced details**", Click on the box under **IAM instance profile** and then select your newly created role. Click on **Create**.
![Launch template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vz05yli7xxewr1vcqnbd.png)

Finally, be sure to set the new version as the default version.

![Launch template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b81zisnhxlsvhbavuq4x.png)

**AND YOU're DONE !!!!!!!**
Our three-tier application should now be up and running.


## Step 9: Accessing Your Application
If you have followed through with all these steps, Congrats.
Now to access your application, you have to visit the EC2 console to get the domain name of your load balancer.

Go to the **EC2 Console** and click on **Load Balancers**.

![EC@ Load balancer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/te703triqyvnirl09fsi.png)

**Copy the DNS Name**

![AWS EC2 Console: Load balancer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r0uuqkpqm5wo34egjf6u.png)

**Now paste this into a web browser.**


![Flask app running on AWS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxbewljcfhie0yv48jw2.png)

![Flask app running on AWS using 3-tier architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pe4dbp7q55xorssua9tg.png)


![Flask Appplication running on AWs using 3-tier architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlg1wrg19ydso2oiiyw9.png)


That's it. If you made it this far, Congratulations.