Mastering AWS: Deploying a Complex Three-Tier Architecture for Production
Tags: aws, devops, tutorial, cicd
Published: 2023-07-14 20:00:03

**Multi-tiered architectures** have become extremely popular for designing and building applications in the cloud. These architectures offer a wide range of benefits such as high availability, replication, flexibility, and security. In contrast, single-tier architectures involve packaging all the components of a software application into a single server.

The most widely used multi-tier design pattern is the three-tier architecture. This architecture consists of three layers: the Presentation Layer, the Logic Layer, and the Data Layer.

- The **Presentation Layer** is the outermost layer of the application and provides an interface for user interaction. It also establishes a secure communication channel with the other layers.

- The **Logic Layer** is responsible for processing information and executing application logic. It receives input from the presentation layer, processes it, and communicates with the data layer when necessary. It is also known as the application layer or middleware.

- The **Data Layer** is where the database management system resides, providing a secure and isolated environment for storing and managing application information.

In this guide, we will design a highly fault-tolerant and scalable Flask application on AWS using the three-tier architecture pattern.

## Architectural Design

👣 The `Presentation layer` will include `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 consist 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, which technically resides in this layer as well. 💼

### Bonus:

- <a href="https://aws.amazon.com/route53/" target="_blank">AWS Route 53</a> will be used to provide a domain name for the application.
- We will integrate <a href="https://aws.amazon.com/cloudfront/" target="_blank">CloudFront</a> with the Application Load Balancer to provide worldwide accelerated content delivery and reduce latency.
- <a href="https://https://github.com/apotitech/terraform-prod-3tier/tree/main/terraform" target="_blank">Terraform</a> will be used as the Infrastructure-as-code (IAC) tool to automate the entire process.

To automatically create this architecture from start to finish, click on this [Link](https://https://github.com/apotitech/terraform-prod-3tier) to access the Terraform code.

This is the first part of a three-series project. **In the second project, we will implement DevOps on AWS using CodePipeline, CodeBuild, and CodeDeploy**. The third project will introduce a Reporting Layer to the architecture.

This part of the guide focuses solely on designing and deploying a three-tier architecture for a Flask application on AWS.

Let's get started, shall we?

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/hajime3tier.gif "text")

## Step 1: Let us Create the Environment

First things first, let's dive into the world of VPCs! A <a href="https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html" target="_blank">VPC</a>, also known as a virtual private cloud, is like having your own little secure haven in the vast public Cloud. It's basically a virtual data center that exists in the cloud, where all your application resources can happily reside. **Exciting, isn't it**? 😄

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t1.png "text")

My VPC is named project-x and all resources created within it will have the prefix '`project-x`'. Additionally, I have assigned a CIDR block of `10.0.0.0/16` to my VPC, which provides a range of private IP addresses for my VPC resources.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t2.png "text")

In the `us-east-1` region (North Virginia), our VPC will be provisioned in `three availability zones`. This will result in the creation of a total of `six subnets`, consisting of three public and three private subnets. The public subnets are made public by attaching an internet gateway to them.

To enable our `EC2` instances hosting our application to connect to the internet for updates, we are creating a NAT gateway. This `NAT gateway` will serve as a connection point for the instances.

## Step 2: Create A Security Group

In order to control the traffic entering our web servers hosted in the logic-tier, we are setting up a security group named '`project-x-logic-tier-sg`'. This security group acts like a firewall and allows us to determine how traffic enters or exits the instances.

Specifically, we need to allow inbound traffic on `port 5000`, as Flask listens on this port for communication.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t3.png "text")

## Step 3: Creating a Launch Template

A <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/launch-templates.html" target="_blank">Launch Template</a> is a convenient way to specify important configuration details for launching instances. In this case, we will use the Launch Template to create instances in our Application tier within an auto scaling group.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t4.png "text")

To create the `Launch Template`, I assigned a name and description to it. I also tagged the environment as '`prod`' to easily identify it.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t5.png "text")

I selected the '`t3.xlarge`' instance type and made sure to choose the '`project-x-logic-tier-sg`' security group. I left all other settings as they were.

In the '`Advanced details`' section, I added the following user data:

```sh
#!/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: Creating an Autoscaling Group, Elastic Load Balancer, and Target Group

The Load Balancer serves as the entry point to the application. In this case, we will use an <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" target="_blank">Application Load Balancer</a> in the presentation layer. It will route traffic through the <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-asg-launch-template.html" target="_blank">Autoscaling Group</a> to instances in the logic layer.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t6.png "text")

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t7.png "text")

To create the `Autoscaling Group`, I named it '`project-x-asg`'. I selected the '`project-x-vpc`' we created earlier and chose only the private subnets from the three availability zones. This is crucial as our VPC will be launched in the `logic` tier, and the subnets must not be public.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t8.png "text")

Under '`Configure advanced options`', I selected '`Attach to a new load balancer`' and created a target group. On the next page, I configured the ASG with a `minimum capacity of 2`, a `desired capacity of 4`, and a `maximum capacity of 6`. I also set up scaling based on target tracking metrics, specifically average CPU utilization.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t9.png "text")

Adding tags is optional, but I decided to add a tag with the name '`Environment`' and a value of '`Prod`'. This will be useful during the CodeDeploy stage.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t10.png "text")

Once all the configurations were set, I created the **ASG**.

## Step 5: Attaching an Elastic Filesystem

<a href="https://aws.amazon.com/efs/" target="_blank">AWS EFS</a> is a managed shared storage solution in the cloud that is compatible with NFS. In our three-tier architecture, it sits in the data layer and provides shared storage for the application tier servers.

Before creating the EFS, I created a new security group that allows inbound NFS traffic only from the security group of our logic-tier instances. Click **<a href="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." target="_blank">here</a>** for a detailed guide on how to.

In the EFS console, I clicked on '`Create filesystem`' and selected '`Customize`' to configure the specific settings for our EFS.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t11.png "text")

Name your `EFS` according to your preference and keep all other settings as default. Proceed to the next step.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t12.png "text")

In the "`Network access`" page, choose your "`project-x`" VPC and select the EFS security group you've created. Click on the next button.

For the "`Filesystem Policy`", there is no need to make any changes. Leave everything as default.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t13.png "text")

Move on to the "`Review`" section and create your filesystem.
Now, let's update our user data to match the following format:

```sh
#!/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's where you can find the DNS name of your `filesystem`. 

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t14.png "text")

Make sure to update the value of the '**fsname**' variable to match your own filesystem's `DNS` name.

## Step 6: Create A Bastion Host

A <a href="https://dev.to/aws-builders/bastion-host-in-aws-vpc-2i63" target="_blank">Bastion Host</a>, also known as a Jump Box or Jump Server, is a special server used to manage access to servers in an internal network or other private AWS resources from an external network. The bastion host is located in the public network and provides limited access to administrators, allowing them to log in to servers in an isolated network. This is particularly useful for gaining SSH access to our application layer servers for administrative purposes.

If you're interested in creating your own bastion host and connecting to your logic layer servers through it, check out this resource: <a href="https://www.strongdm.com/blog/bastion-hosts-with-audit-logging-part-one" target="_blank">Here</a>.

However, it's important to note that connecting to bastion hosts using ssh key-pairs is no longer recommended. Instead, it is advised to use <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html" target="_blank">AWS Systems Manager</a> for a more secure connection and tunneling through the bastion host to your private instances.

## Step 7: Creating the Database

The database is an essential component of the data layer, along with the Elastic filesystem. In our project, we will be using MYSQL as our relational database management system (DBMS). <a href="https://dev.mysql.com/doc/refman/8.0/en/what-is-mysql.html" target="_blank">MYSQL</a> is a widely used DBMS that is free and open source. It is known for its ability to support large production workloads, making it a suitable choice for our project.

However, instead of setting up MYSQL ourselves, we will be utilizing <a href="https://aws.amazon.com/rds/mysql/" target="_blank">Amazon RDS for MYSQL</a>, which is a fully managed database solution in the cloud.

To create the database, we need to go to the RDS Console and click on "`Databases`". From there, we can click on "`Create database`".

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t15.png "text")

I have selected a "`Production`" template and chosen "Multi-AZ DB-instance" for added availability and redundancy.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t16.png "text")

Next, we need to select a master username and DB Instance Class. We will be using AWS Secrets Manager to manage our DB credentials securely.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t17.png "text")

Under "`Connectivity`", we should select the "`project-x-vpc`"and click on "`Create a new security group`".

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t18.png "text")

In the security group, we should only allow incoming traffic on port `3306` from the security group of the application layer servers. Once we have set up the security group, we can select it.

For "`Database Authentication`", we should choose "`password`".

Under "`Advanced Options`", we should choose "`newsreadb`" as the database name. It is important to choose this exact name for the Flask App to be able to connect to the database.

Finally, we can click on "`Create database`".

With this, our architecture is almost ready!

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t19.jpg "text")

To establish a connection with the database, our amazing application will gracefully execute API calls to AWS Secrets Manager, which will effortlessly retrieve the DB credentials. As a result, it is imperative that we fashion an <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html" target="_blank">IAM</a> role that grants the application the ability to perform this action flawlessly. Additionally, we must elegantly tweak the launch template to effortlessly attach the role as an instance profile for our beloved application layer servers.

## Step 8: Creating an IAM Role and Modifying the Launch Template

To do this, we need to go to the IAM Dashboard and click on "`Roles`". From there, we can click on "`Create Role`". Under "`UseCase`", we should select `EC2` and click "`Next`". Then, we can search for "`secrets`" and select the permission that appears.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t20.png "text")

After clicking "`Next`" and "`Create Role`". We can proceed to modify our launch template. We need to go to the EC2 Console, select launch templates, and click on "`Modify launch template`".

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t21.png "text")

In the "`Advanced details`" section, we should click on the box under "`IAM instance profile`" and select the newly created role. Once done, we can click on "`Create`".

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t22.png "text")

Finally, it is `important` to set the new version of the launch template as the default version.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t23.png "text")

> **And that's it! You Are .. Done!!!!**

*Our three-tier application should now be up and running*.

## Step 9: Accessing Your Application

Congratulations if you've completed all the steps! Now, to access your application, simply follow these instructions:

1. Go to the EC2 Console and click on Load Balancers.
2. Copy the DNS Name provided.
3. Paste the DNS Name into a web browser.

Go to the EC2 Console and click on Load Balancers.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t24.png "text")

Copy the DNS Name provided.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t25.png "text")

Paste the DNS Name into a web browser.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t26.png "text")
![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t27.png "text")
![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t28.png "text")

That's it! You can now enjoy running your Flask application on AWS using the 3-tier architecture. 

> **It's been a journey, and you've made it this far. Congratulations once again!**

## Now let's move on to the Bonus Section.

First and foremost, you can find the `Terraform` code that automates this entire process from start to finish <a href="https://github.com/apotitech/terraform-prod-3tier" target="_blank">here</a>.

Next, we will set up a domain name for our application using `AWS Route53`. Additionally, we will integrate our `Application Load Balancer` with `CloudFront`, which will greatly enhance content delivery to our users. This step is crucial if you plan to run this architecture in a production environment.

### Step 1: Create a Domain Name

To proceed with the rest of this project, you will need an active domain name. You have the option to purchase a domain directly from [Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html), or you can choose other providers such as [GoDaddy](https://www.godaddy.com/en-uk/domains) or [Namecheap](https://www.namecheap.com/).

### Step 2: Create a Hosted Zone

A hosted zone serves as a container for records that define how you want to route traffic to your domain. To create a public hosted zone, go to the Route53 Console, click on "Hosted Zones,"and fill in the required fields with your values.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t29.png "text")

### Step 3: Create an SSL Certificate

An <a href="https://www.kaspersky.com/resource-center/definitions/what-is-a-ssl-certificate" target="_blank">SSL/TLS certificate</a> is a digital object that enables systems to verify the identity and establish an encrypted network connection using the SSL/TLS protocol. AWS Certificate Manager is a service that provisions and manages public and private SSL/TLS certificates. 

To get started, go to the Certificate Manager Console and click on "`Request Certificate.`" Enter your fully qualified domain name and click "`Request`." It may take some time for AWS to approve this request, especially if you purchased your domain name outside of AWS. Please be patient during this process. 

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t30.png "text")

Note that this certificate is needed for provisioning your CloudFront distribution in a production environment. However, for the purposes of this project, we will not be using HTTPS, so this step is not necessary.

### Step 4: Create a CloudFront Distribution and Integrate with your ALB

AWS CloudFront is a <a href="https://www.cloudflare.com/learning/cdn/what-is-a-cdn/" target="_blank">Content Delivery Network</a> (CDN) that globally delivers your data to users with minimal latency by routing requests through the nearest edge location. We will integrate CloudFront with our ALB to enhance dynamic content delivery. In a production environment, it is recommended to implement SSL termination on the ALB. However, since we are not using SSL certificates in this project, you can skip this step.

To create a CloudFront distribution, go to the CloudFront Console and click on "`Create Distribution`." Select your `Application Load Balancer` as the Origin Domain. 

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t31.png "text")

Choose "`HTTP Only`". leave the remaining fields as default. 

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t32.png "text")

Select "`CachingDisabled`" as your caching policy and set the origin request policy to "`AllViewer`."

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t33.png "text")

Click on "`Create Distribution.`"

### Step 5 (Final Step): Copy the Distribution Domain Name and Paste it in a Web Browser

Now, let's wrap things up! Copy the distribution domain name from the CloudFront Console and paste it into a web browser. Voila! You should now see your Flask application running on AWS.

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t34.png "text")

> **Aaaaaaaaaaaaand ..................... Success!!!!!**

![text](https://github.com/apotitech/photos_document_md/raw/master/src/common/photos/3t35.png "text")

## Conclusion

In wrapping up, three-tier architectures are a well-established pattern for software application deployment. They ensure resilience, fault tolerance, and security in the application architecture. 

Having completed this project, you should feel confident in designing and deploying similar architectures on AWS. 

Don't forget to check out the second part of this project series, which focuses on CI/CD. 

Happy Building!!

> Thank you for taking the time to read this! If you like the article, please clap (up to 50 times!) and connect with me on [LinkedIn](https://www.linkedin.com/in/apotitech-b79097210/) and [Medium](https://medium.com/@softwaresennin) to remain up to speed on my future articles. 😅