How to Deploy a Highly Available Three-Tier Architecture in AWS using Terraform

Achintha Bandaranaike
17 min readMay 1, 2023

--

Introduction:

When building a cloud-based application, it’s critical to consider the underlying architecture and environment to ensure scalability, availability, and security. Using Infrastructure-as-Code (IaC) tools like Terraform has become increasingly popular for automating the deployment and management of cloud resources.

In this article, we’ll explore how to deploy a highly available three-tier architecture in AWS using Terraform. Our architecture will consist of an EC2 Auto Scaling group for our web tier and app tier, an RDS MySQL database for our data tier, and a bastion host for secure remote access.

By using Terraform, we can easily and efficiently deploy and manage our resources while ensuring our architecture is scalable, highly available, and secure. Let’s dive in and explore how to deploy a highly available three-tier architecture in AWS using Terraform.

What is Three-Tier Architecture and Why Three-Tier?

A Three-Tier Architecture is a popular architectural pattern that can provide increased scalability, availability, and security for cloud-based applications. This architecture is designed to separate an application into three distinct layers, each with a specific function, which is independent of the other. By spreading the application across multiple Availability Zones and separating it into these three layers, the application can achieve high availability and resilience.

If one Availability Zone goes down for any reason, the application can automatically scale resources to another Availability Zone without affecting the rest of the application. Each tier has its own security group that only allows the necessary inbound and outbound traffic to perform its specific tasks. This architecture is an effective way to address many of the challenges faced by cloud-based applications.

Three-tier architecture is a client-server architecture consisting of three layers or tiers: The web layer, The Application layer, and The Data storage layer.

  1. Web/Presentation Tier (Front End): Houses the user-facing elements of the application, such as web servers and the interface/front-end.
  2. Application Tier (Back End): Houses the backend and application source code needed to process data and run functions.
  3. Data Tier: Houses and manages the application data. Often where the databases are stored.

Prior reading:

For those who want to start with a simpler architecture before moving on to three-tier, my article “How to Deploy a Two-Tier Architecture in AWS using Terraform” provides a step-by-step guide on deploying an EC2 instance and an RDS database instance in a VPC, as well as setting up security groups and VPC routing. This can serve as a great starting point for building a more complex and robust three-tier architecture.

The scenario

The scenario is wanting to build a new web app. You’re tasked with planning and building the architecture the application will be run on.

Let’s get started!

Prerequisites

  • An AWS account with IAM user access.
  • Code Editor (I used VS Code for this deployment)
  • Familiarity with Linux commands, scripting, and SSH.
  • Reference: https://registry.terraform.io/

Architecture Diagram:

This three-tier architecture will contain the following components:

  • Deploy a VPC with CIDR 10.0.0.0/16
  • Within the VPC we will have 2 public subnets with CIDR 10.0.0.0/28 and 10.0.0.16/28. Each public subnet will be in a different Availability Zone for high availability.
  • Create 4 private subnets with CIDR 10.0.0.32/28, 10.0.0.48/18 for the application tier and CIDR 10.0.0.64/28, 10.0.0.80/28 for the database tier and each will be in a different Availability Zone.
  • Deploy RDS MySQL instance.
  • An Application load balancer that will direct traffic to the public subnets and another application load balancer to handle traffic from the web tier to the app tier.
  • Deploy one EC2 auto-calling group in each public subnet(web tier)and private subnet (app tier) for high availability.
  • Internet Gateway, NAT gateway, and Elastic IPs for EC2 instance.
  • Bastion host for connecting direct app servers.

Let’s get started!

Create provider.tf file

The provider.tf file in Terraform is a configuration file that specifies the cloud provider and its corresponding plugin that Terraform will use to manage resources in that provider.

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>4"
}
}
}

provider "aws" {
region = "ap-southeast-1"
}

1. Create Network Architecture

In creating network architecture all the resources are deployed in network resources.tf file. Basically, Network architecture includes the below aws services.

  • A VPC.
  • Two (2) public subnets spread across two availability zones (Web Tier).
  • Two (2) private subnets spread across two availability zones (Application Tier).
  • Two (2) private subnets spread across two availability zones (Database Tier).
  • One (1) public route table that connects the public subnets to an internet gateway.
  • One (1) private route table that will connect the Application Tier private subnets and a NAT gateway.
Network Architecture

Create network resources.tf file

The network_resources.tf file is an essential component of any Terraform project that aims to deploy a scalable and highly available infrastructure in AWS. This file contains all the necessary network blocks and resources required to create a robust networking layer for your applications. These resources include a Virtual Private Cloud (VPC), two public subnets, four private subnets, an Internet Gateway (IGW), a NAT Gateway, an Application Load Balancer (ALB), and two public and private route tables.

The VPC is the foundation of the networking infrastructure and provides a private and isolated environment for your resources to operate. The public subnets are designed to host resources that require public access, while the private subnets are used for resources that require restricted access. The IGW provides a path for incoming and outgoing traffic to and from the public subnets, while the NAT Gateway allows resources in the private subnets to access the internet. The ALB provides load balancing for your applications, distributing traffic across multiple instances to ensure optimal performance and scalability. Finally, the public and private route tables control the routing of traffic between the subnets and gateways, ensuring that traffic flows seamlessly through the network.

By using Terraform to manage these resources, you can easily create and maintain a highly available and scalable network infrastructure in AWS, allowing your applications to run smoothly and reliably.

a). Create VPC, Public Subnets, and Private Subnets

# VPC
resource "aws_vpc" "three-tier-vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "three-tier-vpc"
}
}

# Public Subnets
resource "aws_subnet" "three-tier-pub-sub-1" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.0/28"
availability_zone = "ap-southeast-1a"
map_public_ip_on_launch = "true"

tags = {
Name = "three-tier-pub-sub-1"
}
}

resource "aws_subnet" "three-tier-pub-sub-2" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.16/28"
availability_zone = "ap-southeast-1b"
map_public_ip_on_launch = "true"
tags = {
Name = "three-tier-pub-sub-2"
}
}


# Private Subnets
resource "aws_subnet" "three-tier-pvt-sub-1" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.32/28"
availability_zone = "ap-southeast-1a"
map_public_ip_on_launch = false
tags = {
Name = "three-tier-pvt-sub-1"
}
}
resource "aws_subnet" "three-tier-pvt-sub-2" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.48/28"
availability_zone = "ap-southeast-1b"
map_public_ip_on_launch = false
tags = {
Name = "three-tier-pvt-sub-2"
}
}

resource "aws_subnet" "three-tier-pvt-sub-3" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.64/28"
availability_zone = "ap-southeast-1a"
map_public_ip_on_launch = false
tags = {
Name = "three-tier-pvt-sub-3"
}
}
resource "aws_subnet" "three-tier-pvt-sub-4" {
vpc_id = aws_vpc.three-tier-vpc.id
cidr_block = "10.0.0.80/28"
availability_zone = "ap-southeast-1b"
map_public_ip_on_launch = false
tags = {
Name = "three-tier-pvt-sub-4"
}
}

b). Create Internet Gateway

# Internet Gateway
resource "aws_internet_gateway" "three-tier-igw" {
tags = {
Name = "three-tier-igw"
}
vpc_id = aws_vpc.three-tier-vpc.id
}

c). Create Route Tables

To enable communication between the web tier and the application tier, we need to create two route tables: one for each tier. Route tables define which subnet traffic is allowed to flow to and from. We’ll create a public route table for the web tier, which will contain the public subnet associated with our load balancer. We’ll also create a private route table for the application tier, which will contain the private subnets associated with our application servers. By associating these route tables with the appropriate subnets, we ensure that traffic flows only to the intended destination. The web servers and app servers are connected to these two route tables, enabling them to communicate with each other as needed.

# Create a Route Table
resource "aws_route_table" "three-tier-web-rt" {
vpc_id = aws_vpc.three-tier-vpc.id
tags = {
Name = "three-tier-web-rt"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.three-tier-igw.id
}
}

resource "aws_route_table" "three-tier-app-rt" {
vpc_id = aws_vpc.three-tier-vpc.id
tags = {
Name = "three-tier-app-rt"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.three-tier-natgw-01.id
}
}


# Route Table Association
resource "aws_route_table_association" "three-tier-rt-as-1" {
subnet_id = aws_subnet.three-tier-pub-sub-1.id
route_table_id = aws_route_table.three-tier-web-rt.id
}

resource "aws_route_table_association" "three-tier-rt-as-2" {
subnet_id = aws_subnet.three-tier-pub-sub-2.id
route_table_id = aws_route_table.three-tier-web-rt.id
}

resource "aws_route_table_association" "three-tier-rt-as-3" {
subnet_id = aws_subnet.three-tier-pvt-sub-1.id
route_table_id = aws_route_table.three-tier-app-rt.id
}
resource "aws_route_table_association" "three-tier-rt-as-4" {
subnet_id = aws_subnet.three-tier-pvt-sub-2.id
route_table_id = aws_route_table.three-tier-app-rt.id
}

resource "aws_route_table_association" "three-tier-rt-as-5" {
subnet_id = aws_subnet.three-tier-pvt-sub-3.id
route_table_id = aws_route_table.three-tier-app-rt.id
}
resource "aws_route_table_association" "three-tier-rt-as-6" {
subnet_id = aws_subnet.three-tier-pvt-sub-4.id
route_table_id = aws_route_table.three-tier-app-rt.id
}

# Create an Elastic IP address for the NAT Gateway
resource "aws_eip" "three-tier-nat-eip" {
vpc = true
}

d). Create a NAT gateway

A NAT gateway allows instances from the private subnets to connect to resources outside of the VPC and the Internet (for necessary services such as patches or package updates).

It’s best practice to maintain a high availability and deploy two NAT gateways in our public subnets (one in each AZ) So however, for now, I will just deploy one.

#NatGW
resource "aws_nat_gateway" "three-tier-natgw-01" {
allocation_id = aws_eip.three-tier-nat-eip.id
subnet_id = aws_subnet.three-tier-pub-sub-1.id

tags = {
Name = "three-tier-natgw-01"
}
depends_on = [aws_internet_gateway.three-tier-igw]
}

2. Tier 1: Web Tier (Frontend)

The Web Tier, also known as the ‘Presentation’ tier, is the environment where our application will be delivered for users to interact with. For Brainiac, this is where we will launch our web servers that will host the front end of our application.

Web Tier — frontend

What we’ll build:

  • A launch template to define what kind of EC2 instances will be provisioned for the application.
  • An Auto Scaling Group (ASG) that will dynamically provision EC2 instances.
  • An Application Load Balancer (ALB) to help route incoming traffic to the proper targets.
  • A security group to control inbound and outbound traffic to the web servers.

To ensure the Web Tier is highly available and can handle traffic fluctuations, we will use an ASG to dynamically provision EC2 instances. The launch template will define what kind of EC2 instances will be created, including the instance size, AMI, and security group.

To ensure traffic is distributed evenly across our web servers and to provide high availability, we will use an Application Load Balancer. The ALB will monitor the health of our web servers and direct traffic to the available instances.

Finally, we will create a security group for our web servers. This security group will control inbound and outbound traffic to the instances, limiting access to only necessary ports and sources. This will help prevent unauthorized access and reduce the risk of security breaches.

a). Create EC2 Auto Scaling Group and Launch Template

To ensure high availability for the web app and limit single points of failure, we will create an ASG that will dynamically provision EC2 instances, as needed, across multiple AZs in our public subnets and create a template that will be used by our ASG to dynamically launch EC2 instances in our public subnets.

######### Create an EC2 Auto Scaling Group - web ############
resource "aws_autoscaling_group" "three-tier-web-asg" {
name = "three-tier-web-asg"
launch_configuration = aws_launch_configuration.three-tier-web-lconfig.id
vpc_zone_identifier = [aws_subnet.three-tier-pub-sub-1.id, aws_subnet.three-tier-pub-sub-2.id]
min_size = 2
max_size = 3
desired_capacity = 2
}

###### Create a launch configuration for the EC2 instances #####
resource "aws_launch_configuration" "three-tier-web-lconfig" {
name_prefix = "three-tier-web-lconfig"
image_id = "ami-0b3a4110c36b9a5f0"
instance_type = "t2.micro"
key_name = "three-tier-web-asg-kp"
security_groups = [aws_security_group.three-tier-ec2-asg-sg.id]
user_data = <<-EOF
#!/bin/bash

# Update the system
sudo yum -y update

# Install Apache web server
sudo yum -y install httpd

# Start Apache web server
sudo systemctl start httpd.service

# Enable Apache to start at boot
sudo systemctl enable httpd.service

# Create index.html file with your custom HTML
sudo echo '
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>A Basic HTML5 Template</title>

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap"
rel="stylesheet"
/>

<link rel="stylesheet" href="css/styles.css?v=1.0" />
</head>

<body>
<div class="wrapper">
<div class="container">
<h1>Welcome! An Apache web server has been started successfully.</h1>
<h2>Achintha Bandaranaike</h2>
</div>
</div>
</body>
</html>

<style>
body {
background-color: #34333d;
display: flex;
align-items: center;
justify-content: center;
font-family: Inter;
padding-top: 128px;
}

.container {
box-sizing: border-box;
width: 741px;
height: 449px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 48px 48px 48px 48px;
box-shadow: 0px 1px 32px 11px rgba(38, 37, 44, 0.49);
background-color: #5d5b6b;
overflow: hidden;
align-content: flex-start;
flex-wrap: nowrap;
gap: 24;
border-radius: 24px;
}

.container h1 {
flex-shrink: 0;
width: 100%;
height: auto; /* 144px */
position: relative;
color: #ffffff;
line-height: 1.2;
font-size: 40px;
}
.container p {
position: relative;
color: #ffffff;
line-height: 1.2;
font-size: 18px;
}
</style>
' > /var/www/html/index.html

EOF

associate_public_ip_address = true
lifecycle {
prevent_destroy = true
ignore_changes = all
}
}

b). Create Application Load Balancer — App tier (network resources.tf)

We’ll need an ALB to distribute incoming HTTP traffic to the proper targets (our EC2s). The ALB needs to ‘listen’ over HTTP on port 80 and a target group that routes to our EC2 instances. We want to set a minimum and maximum number of instances the ASG can provision:

  • Desired capacity: 2
  • Minimum capacity: 2
  • Maximum capacity: 3
# Create Load balancer - web tier
resource "aws_lb" "three-tier-web-lb" {
name = "three-tier-web-lb"
internal = true
load_balancer_type = "application"

security_groups = [aws_security_group.three-tier-alb-sg-1.id]
subnets = [aws_subnet.three-tier-pub-sub-1.id, aws_subnet.three-tier-pub-sub-2.id]

tags = {
Environment = "three-tier-web-lb"
}
}

# create load balancer larget group - web tier

resource "aws_lb_target_group" "three-tier-web-lb-tg" {
name = "three-tier-web-lb-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.three-tier-vpc.id

health_check {
interval = 30
path = "/"
port = "traffic-port"
protocol = "HTTP"
timeout = 10
healthy_threshold = 3
unhealthy_threshold = 3
}
}

# Create Load Balancer listener - web tier
resource "aws_lb_listener" "three-tier-web-lb-listner" {
load_balancer_arn = aws_lb.three-tier-web-lb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.three-tier-web-lb-tg.arn
}
}

# Register the instances with the target group - web tier
resource "aws_autoscaling_attachment" "three-tier-web-asattach" {
autoscaling_group_name = aws_autoscaling_group.three-tier-web-asg.name
alb_target_group_arn = aws_lb_target_group.three-tier-web-lb-tg.arn

}

3. Tier 2: Application Tier (Backend)

The Application Tier is where the core functionality of our application app resides, handling tasks such as processing requests and data storage. To ensure the reliability and scalability of our application, we will implement a similar architecture as the Web Tier, but with some additional components.

Application Tier (Back End)

What We Will Build:

To create the Application Tier, we will build the following components:

  • Launch Template: We will define a launch template that specifies the type of EC2 instances that will be provisioned for our application. This launch template can be used to quickly launch new instances with the necessary configuration and settings.
  • Auto Scaling Group (ASG): To ensure high availability and scalability, we will create an ASG that will automatically adjust the number of EC2 instances in the Application Tier based on traffic and load. This will allow us to maintain optimal performance and handle sudden increases in traffic.
  • Application Load Balancer (ALB): To route traffic from the Web Tier to the Application Tier, we will set up an ALB. The ALB will receive incoming requests and distribute them to the appropriate EC2 instances in the Application Tier, helping to balance the workload and ensure availability.
  • Bastion Host: To securely connect to our application servers, we will create a Bastion host. The Bastion host acts as a jump server, allowing us to connect to the Application Tier instances without exposing them to the public internet.

By creating the Application Tier, we can ensure that our app is scalable, resilient, and can handle a high volume of requests and data.

a). Create EC2 Auto Scaling Group and Launch Template

To ensure high availability for the web app and limit single points of failure, we will create an ASG that will dynamically provision EC2 instances, as needed, across multiple AZs in our public subnets and create a template that will be used by our ASG to dynamically launch EC2 instances in our private subnets. Our security group settings are where things will differ. Remember, this is a private subnet, where all of our application source code will live. We need to take precautions so it cannot be accessible from the outside. We want to allow ICMP–IPv4 from the web tier security group, which allows us to ping the application server from our web server.

Terraform file: ec2 resources.tf

# Create an EC2 Auto Scaling Group - app
resource "aws_autoscaling_group" "three-tier-app-asg" {
name = "three-tier-app-asg"
launch_configuration = aws_launch_configuration.three-tier-app-lconfig.id
vpc_zone_identifier = [aws_subnet.three-tier-pvt-sub-1.id, aws_subnet.three-tier-pvt-sub-2.id]
min_size = 2
max_size = 3
desired_capacity = 2
}

# Create a launch configuration for the EC2 instances
resource "aws_launch_configuration" "three-tier-app-lconfig" {
name_prefix = "three-tier-app-lconfig"
image_id = "ami-0b3a4110c36b9a5f0"
instance_type = "t2.micro"
key_name = "three-tier-app-asg-kp"
security_groups = [aws_security_group.three-tier-ec2-asg-sg-app.id]
user_data = <<-EOF
#!/bin/bash

sudo yum install mysql -y

EOF

associate_public_ip_address = false
lifecycle {
prevent_destroy = true
ignore_changes = all
}
}

b). Create a Bastion host

A bastion host is a dedicated server used to securely access a private network from a public network. We want to protect our Application Tier from potential outside access points, so we will create an EC2 instance in the Web Tier, outside of the ASG. This is the only server that will be used as a gateway to our app servers.

4. Tier 3: Database tier (Data storage & retrieval)

The Database Tier is where our application will store all the important data, such as user login info, session data, transactions, and application content. Application servers need to be able to read and write to databases to perform necessary tasks and deliver proper content/services to the Web Tier and users. To accomplish this, we are going to use a Relational Database Service (RDS) that uses MySQL.

Database Tier

What we’ll build:

  1. A database security group that allows outbound and inbound mySQL requests to and from our app servers.
  2. A DB subnet group to ensure the database is created in the proper subnets.
  3. An RDS database with MySql.

With these components in place, we can ensure that our Database Tier is secure and accessible to the other tiers of our application. Let’s move on to creating these components in our database resources.tf file.

#### RDS ####
resource "aws_db_subnet_group" "three-tier-db-sub-grp" {
name = "three-tier-db-sub-grp"
subnet_ids = ["${aws_subnet.three-tier-pvt-sub-3.id}","${aws_subnet.three-tier-pvt-sub-4.id}"]
}

resource "aws_db_instance" "three-tier-db" {
allocated_storage = 100
storage_type = "gp3"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t2.micro"
identifier = "three-tier-db"
username = "admin"
password = "23vS5TdDW8*o"
parameter_group_name = "default.mysql8.0"
db_subnet_group_name = aws_db_subnet_group.three-tier-db-sub-grp.name
vpc_security_group_ids = ["${aws_security_group.three-tier-db-sg.id}"]
multi_az = true
skip_final_snapshot = true
publicly_accessible = false

lifecycle {
prevent_destroy = true
ignore_changes = all
}
}

Let’s deploy!

Make sure you have already inserted your AWS credentials and are operating from the root directory before starting these Terraform commands.

  1. terraform init

The terraform init the command is used to initialize a new or existing Terraform configuration. This command downloads the required provider plugins and sets up the backend for storing state.

terraform init

2. terraform plan

The terraform plan the command is used to create an execution plan for the Terraform configuration. This command shows what resources Terraform will create, modify, or delete when applied.

terraform plan

3. terraform apply

The terraform apply the command is used to apply the Terraform configuration and create or modify resources in the target environment.

terraform apply

Go to the AWS console and verify

1. VPC and Network resources

VPC, subnets, route tables, internet gateway, and NAT gateway

2. EC2 instances

All four ec2s are working fine

3. Application Load Balancers

ALBs are Active

4. Target Groups

5. RDS MYSQL Database

Checking Connections:

Once the ASG is fully initialized, we can go to our EC2 dashboard and see that two EC2 instances have been deployed.

Web Tier EC2s

Web-Tier ALB Connection Checking:

To see if our ALB is properly routing traffic, let’s go to its public DNS. We should be able to access the website we implemented when creating our EC2 launch template.

ALB is working

Web-Tier EC2 Connection Checking:

Let’s confirm that we can SSH into our EC2 server.

EC2 is connected and running

We’ve successfully built the architecture for the Web Tier and all the users can directly connect to our web app (web tier).

App-Tier EC2 Connection Checking:

Application servers are up and running. Let’s verify connectivity by pinging the application server from one of the web servers.

SSH into the web server EC2 and ping the private IP address of one of the app server EC2s.

Successfully pinged the app server and received a response

Now log into the web server and connect to one of the app servers

create keypair file inside the webserver( use same credentials)

 cat three-tier-app-asg-kp.pem

open the Vim editor and past keypair credentials which you previously created in the AWS console.

vim three-tier-app-asg-kp.pem

After you paste your credentials, put the cat command and verify those data are available in your newly created key pair file.

 cat three-tier-app-asg-kp.pem

Change your key pair file permissions.

chmod 0600 three-tier-app-asg-kp.pem

Now log into the App Server.

ssh -i "three-tier-app-asg-kp.pem" ec2-user@10.0.0.43

Try to access RDS MYSQL Database via Appserver.

mysql -h <your database endpoint> -P 3306 -u <database username> -p
Successfully access RDS MYSQL Database

Great! We successfully connected to our database from our application server!

Remember to delete your resources (ASG, ALB, DB, NAT Gateway) and release all elastic IPs, so you don’t continue to get charged!

Note:

You can find all of the Terraform code used in this tutorial on the following GitHub repository:

.

Thanks for reading! Let’s see you in the next article. Don’t forget to follow me via medium and leave a 👏

--

--

Achintha Bandaranaike

AWS Community Builder ☁️| Cloud Enthusiast | 3xAWS | 3xAzure | Terraform Certified | 1xGCP