How to deploy a three-tier architecture in AWS using Terraform?

What is Terraform?

CJ Regan
11 min readMar 22, 2022

HashiCorp Terraform is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to provision and manage all of your infrastructure throughout its lifecycle. Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features.

Read more on Terraform HERE!

In this article I will explain how I deployed my three tier architecture using Terraform as pictured below. It creates a simple address book application that is accessible from your browser, that you connect to using the Database endpoint.

I am by no means an expert, I started my journey into DevOps as a junior a month ago this was my first project I completed by myself.

Three Tier Architecture.

Prerequisites:

  • Basic knowledge of AWS & Terraform
  • An AWS account
  • An AWS Access & Secret Key

In this project, I have used some variables also that I will discuss later in this article.

Step 1:- Create a file for the Provider

  • Create provider.tf file and add the below code to it
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = "3.64.0"
}
}
provider "aws" {
region = "eu-west-2"
}

If you wish to change the region that you want to work in, you need to change it in the provider code above. And in the tfvars file later on in this article.

Step 2:- Create a file for the VPC

  • Create vpc.tf file and add the below code to it
# Creating VPCresource "aws_vpc" "prod-vpc" {
cidr_block = var.vpc_cidr
instance_tenancy = "default"
tags = {
Name = "prod"
}
}

Step 3:- Create a file for the Subnet

  • For this project, I will create total 6 subnets for the front-end tier, app tier and back-end tier with a mixture of public & private subnets.
  • Create subnet.tf file and add the below code to it
# Create 1st web subnet 
resource "aws_subnet" "public-subnet-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[0].cidr_block
map_public_ip_on_launch = true
availability_zone = "eu-west-2a"
tags = {
Name = var.subnet_prefix[0].name
}
}
# Create 2nd web subnet
resource "aws_subnet" "public-subnet-2" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[1].cidr_block
map_public_ip_on_launch = true
availability_zone = "eu-west-2b"
tags = {
Name = var.subnet_prefix[1].name
}
}
# Creating 1st application subnet
resource "aws_subnet" "application-subnet-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[2].cidr_block
map_public_ip_on_launch = false
availability_zone = "eu-west-2a"
tags = {
Name = var.subnet_prefix[2].name
}
}
# Creating 2nd application subnet
resource "aws_subnet" "application-subnet-2" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[3].cidr_block
map_public_ip_on_launch = false
availability_zone = "eu-west-2b"
tags = {
Name = var.subnet_prefix[3].name
}
}
# Database Private Subnet
resource "aws_subnet" "database-subnet-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[4].cidr_block
availability_zone = "eu-west-2a"
tags = {
Name = var.subnet_prefix[4].name
}
}
# Database Private Subnet
resource "aws_subnet" "database-subnet2" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet_prefix[5].cidr_block
availability_zone = "eu-west-2b"
tags = {
Name = var.subnet_prefix[5].name
}
}

If you have changed the region different to the one I have specified you also need to change the Availability zones in the code above.

Step 4:- Create a file for the Internet Gateway

  • Create igw.tf file and add the below code to it
#  Create Internet Gatewayresource "aws_internet_gateway" "web-app-gateway" {
vpc_id = aws_vpc.prod-vpc.id
tags = {
Name = "Web App Gateway"
}
}

Step 5:- Create a file for the Elastic IP Addresses

  • Create eip.tf file and add the below code to it
resource "aws_eip" "nat1" {depends_on = [aws_internet_gateway.web-app-gateway]

}
resource "aws_eip" "nat2" {depends_on = [aws_internet_gateway.web-app-gateway]

}

Step 6:- Create a file for the Nat Gateways

  • Create nat-gateways.tf file and add the below code to it
resource "aws_nat_gateway" "gw1" {
allocation_id = aws_eip.nat1.id
subnet_id = aws_subnet.public-subnet-1.id
depends_on = [aws_internet_gateway.web-app-gateway]
}
resource "aws_nat_gateway" "gw2" {
allocation_id = aws_eip.nat2.id
subnet_id = aws_subnet.public-subnet-2.id
depends_on = [aws_internet_gateway.web-app-gateway]
}

Step 7:- Create a file for the Route table

  • Create route-tables.tf file and add the below code to it
#  Create Route Tablesresource "aws_route_table" "public-route-table" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.web-app-gateway.id
}
tags = {
Name = "Public"
}
}
resource "aws_route_table" "private1" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.gw1.id
}
tags = {
Name = "Private1"
}
}
resource "aws_route_table" "private2" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.gw2.id
}
tags = {
Name = "Private2"
}
}
resource "aws_route_table" "private3" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.gw1.id
}
tags = {
Name = "Private3"
}
}
resource "aws_route_table" "private4" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.gw2.id
}
tags = {
Name = "Private4"
}
}

Step 8:- Create a file for the Route table Associations

  • Create a route-tables-association.tf file and add the below code to it
# Associating Route Table for pub subnet 1resource "aws_route_table_association" "pub-route1" {
subnet_id = aws_subnet.public-subnet-1.id
route_table_id = aws_route_table.public-route-table.id
}
# Associating Route Table for pub subnet 2resource "aws_route_table_association" "pub-route2" {
subnet_id = aws_subnet.public-subnet-2.id
route_table_id = aws_route_table.public-route-table.id
}
#Associating Route Table for private subnet 1resource "aws_route_table_association" "private-route1" {
subnet_id = aws_subnet.application-subnet-1.id
route_table_id = aws_route_table.private1.id
}
#Associating Route Table for private subnet 2resource "aws_route_table_association" "private-route2" {
subnet_id = aws_subnet.application-subnet-2.id
route_table_id = aws_route_table.private2.id
}
#Associating Route Table for private Database subnet 1resource "aws_route_table_association" "private-route3" {
subnet_id = aws_subnet.database-subnet-1.id
route_table_id = aws_route_table.private3.id
}
#Associating Route Table for private Database subnet 2resource "aws_route_table_association" "private-route4" {
subnet_id = aws_subnet.database-subnet2.id
route_table_id = aws_route_table.private4.id
}

Step 9:- Create a file for the EC2 Launch Template

  • Create a ec2-launch-template.tf file and add the below code to it
resource "aws_launch_template" "web-server-ec2" {
name = "webServerEc2"
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 8
}
}
instance_type = var.instance_type
image_id = var.image_id
user_data = filebase64("data.sh")
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.ec2-sg.id]
}
}
  • I have used the userdata to configure the EC2 instance, I will discuss the data.sh file later in the article.

If you changed the region from eu-west-2 you need to check you have the correct “image_id” (AMI) to reflect the region you are working in. It’s also good practice to check that it is the right one as AWS do often update AMI ID’s. If so this will need to be changed in the .tfvars file later on in this article.

Step 10:- Create a file for the Security Group for the ALB

  • Create a alb-sg.tf file and add the below code to it
# Create ALB Security Groupresource "aws_security_group" "alb-sg" {
name = "vpc_alb_sg"
description = "Allow web inbound traffic"
vpc_id = aws_vpc.prod-vpc.id
ingress {
description = "all traffic from VPC"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "VPCALBSG"
}
}

Step 11:- Create a file for Security Group for the EC2's

  • Create ec2_sg.tf file and add the below code to it
#create Ec2 security groupresource "aws_security_group" "ec2-sg" {
name = "ec2sg"
description = "Allows ALB to access the EC2 instances"
vpc_id = aws_vpc.prod-vpc.id
ingress {
description = "TLS from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb-sg.id]
}
ingress {
description = "TLS from ALB"
from_port = 8443
to_port = 8443
protocol = "tcp"
security_groups = [aws_security_group.alb-sg.id]

}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "VPCEC2SG"
}
}
  • I have opened ports 80 & 8443 for the inbound connection from the ALB and I have opened all the ports for the outbound connection.

Step 12:- Create a file for Security Group for the Database tier

  • Create database_sg.tf file and add the below code to it
# Create Database Security Groupresource "aws_security_group" "rds-sg" {
name = "RDSSG"
description = "Allows application to access the RDS instances"
vpc_id = aws_vpc.prod-vpc.id
ingress {
description = "EC2 to MYSQL"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.ec2-sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "RDSSG"
}
}
  • I have opened port 3306 for the inbound connection and I have opened all the ports for the outbound connection.

Step 13:- Create a file Application Load Balancer

  • Create alb.tf file and add the below code to it
# Create LoadBalancerresource "aws_lb" "web-alb" {
name = "web-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb-sg.id]
subnets = [aws_subnet.public-subnet-1.id, aws_subnet.public-subnet-2.id]
}resource "aws_lb_target_group" "alb-target-group" {
name = "alb-target-group"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.prod-vpc.id
}
resource "aws_lb_listener" "alb-listener" {
load_balancer_arn = aws_lb.web-alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.alb-target-group.arn
}
}

Step 14:- Create a file for Autoscaling

  • Create autoscaling.tf file and add the below code to it
#Create ASGresource "aws_autoscaling_group" "ec2-asg" {
name = "Launch-Temp-ASG"
max_size = 2
min_size = 2
health_check_grace_period = 300
health_check_type = "EC2"
desired_capacity = 2
vpc_zone_identifier = [aws_subnet.application-subnet-1.id, aws_subnet.application-subnet-2.id]
launch_template{
id = aws_launch_template.web-server-ec2.id
version = "$Latest"
}

lifecycle {
ignore_changes = [load_balancers, target_group_arns]
}
}
# Create a new ALB Target Group attachment
resource "aws_autoscaling_attachment" "tg_attachment" {
autoscaling_group_name = aws_autoscaling_group.ec2-asg.id
alb_target_group_arn = aws_lb_target_group.alb-target-group.arn
}

Step 15:- Create a file for the RDS instance

  • Create rds.tf file and add the below code to it
# Create RDS Instanceresource "aws_db_subnet_group" "db-subnet-group" {
name = "dbsubnetgroup"
subnet_ids = [aws_subnet.database-subnet-1.id, aws_subnet.database-subnet2.id]
tags = {
Name = "My DB subnet group"
}
}
resource "aws_db_instance" "rds" {
db_subnet_group_name = aws_db_subnet_group.db-subnet-group.id
allocated_storage = var.allocated_storage
engine = var.engine_type
engine_version = var.engine_version
instance_class = var.instance_class
multi_az = true
name = "mydb"
username = "admin"
password = "reallygoodpassword"
skip_final_snapshot = true
vpc_security_group_ids = [aws_security_group.rds-sg.id]
}
  • In the above code, you can change the value of username & password
  • multi-az is set to true for the high availability

Step 16:- Create a file for the Outputs

  • Create outputs.tf file and add the below code to it
output "db_instance_endpoint" {
description = "The connection endpoint"
value = aws_db_instance.rds.endpoint
}
output "lb_dns_name" {
description = "The DNS name of the load balancer"
value = "${aws_lb.web-alb.dns_name}"
}

From the above code, I will get the DNS of the application load balancer and the Database instance endpoint.

Step 17:- Create a file for the VPC Flow Logs

  • Create vpc-flow-logs.tf file and add the below code to it
resource "aws_flow_log" "flow-log-test" {
log_destination = aws_s3_bucket.YOURNAME.arn
log_destination_type = "s3"
traffic_type = "ALL"
vpc_id = aws_vpc.prod-vpc.id
}
resource "aws_s3_bucket" "YOURNAME" {
bucket = "YOURNAME"
}

You need to choose your own name for your bucket. And put the same name in all the fields shown above where I have put (YOURNAME).

Step 18:- Create a file for User Data

  • Create data.sh file and add the below code to it
#!/bin/bash -ex
yum -y update
yum -y install httpd php mysql php-mysql
chkconfig httpd on
service httpd start
cd /var/www/html
wget PUT OBJECT URL HERE
tar xvfz app.tgz
chown apache:root /var/www/html/rds.conf.php

!!! For this to work you need to change the url in the user data. In this repository you will find a .tgz file that needs to be put into an s3 bucket in your AWS account. You then need to copy the Object URL from your s3 bucket. (You can find this by clicking on the object, heading to properties and you'll find it in the object overview) Paste the URL in the user data file shown in the code above. You also need to make sure that the object is publicly accessible.

The script is run the first time the instance/s are launched. It installs a web server on your EC2 instances/s, and runs an app that can be configured to point to your MySQL RDS instance. After you configure your RDS instance, it will present an address book that you can edit.

Step 19:- Create a file for variables

  • Create variables.tf file and add the below code to it
variable "region" {
type = string
description = "The AWS region."
}
variable "instance_type" {
type = string
description = "The instance type."
default = "t2.small"
}
variable "image_id" {
description = "the ami"
}
variable "subnet_prefix" {
description = "cidr block for the subnet"
}
variable "vpc_cidr" {
description = "cidr block for vpc"
default = "10.0.0.0/16"
}
variable "allocated_storage" {
type = number
description = "The allocated storage for rds"
}
variable "engine_type" {
type = string
description = "Engine for the RDS Instance"
}
variable "engine_version" {
description = "Engine version for the RDS engine type"
}
variable "instance_class" {
type = string
description = "The instance class for the RDS instance"
}

Variables are really helpful in terraform…Variables let you customise aspects of Terraform modules without altering the module’s own source code. This allows you to share modules across different Terraform configurations, making your module composable and reusable. DRY!

Step 20:- Create a file for tfvars

  • Create terraform.tfvars file and add the below code to it
region = "eu-west-2"
image_id = "ami-0dd555eb7eb3b7c82"
subnet_prefix = [{cidr_block = "10.0.1.0/24", name = "Web_Subnet_1"}, {cidr_block = "10.0.2.0/24", name = "Web_Subnet_2"},
{cidr_block = "10.0.11.0/24", name = "App_Subnet_1"}, {cidr_block = "10.0.12.0/24", name = "App_Subnet_2"},
{cidr_block = "10.0.13.0/24", name = "Database_subnet_1"}, {cidr_block = "10.0.14.0/24", name = "Databse_Subnet_2"}]
allocated_storage = 10
engine_type = "mysql"
engine_version = "5.7.31"
instance_class = "db.t3.micro"

If you changed the Region, then you need to retrospectively change the AMI ID to reflect that change. This needs to be changed in the code above.

Now we have all the files needed to create our infrastructure and application. All we need to do is run it!

  • Once you have configured your AWS credentials then follow these steps.

In your terminal run these commands in order…

  • terraform init to initialise the working directory and download the plugins of the provider
  • terraform plan is to create the execution plan for our code
  • terraform apply is to create the actual infrastructure.

Step 13:- Verify the resources

  • Terraform will create the resources below
  1. VPC
  2. Application Load Balancer
  3. Public & Private Subnets
  4. Launch Template
  5. EC2 Instances
  6. NAT Gateways
  7. Elastic Ip’s
  8. Autoscaling group
  9. Route Tables
  10. Internet Gateway
  11. RDS Instance
  12. S3 Bucket & Flow Log
  13. Security Groups for Web & RDS instances

Once the resource creation finishes you can get the DNS of the load balancer and paste it into a browser, you will then need to input the Database endpoint, DB name, username and password. This will then show your address book which you can then input and delete values as you see fit.

That’s it now, you have learned how to create various resources in AWS using Terraform. You can further explore Terraform here

You can find the complete code in my git account here.

If you found this guideline helpful then do click on 👏 the button and also feel free to drop a comment.

Over and out 👩🏽‍💻 🖖

--

--