Deploy Three tier application with Terraform & AWS

What is three-tier Tier Architecture?

Before moving on, it's essential to understand what three-tier architecture is. If you're already familiar with it, you can skip this part. Three-tier architecture is a deployment design pattern in the cloud that organizes applications into three layers:

  1. Presentation Tier: This is the web or user interface where clients interact with the application.

  2. Application Tier: This layer contains all the business logic, implemented using backend technologies such as PHP, Golang, or Django.

  3. Data Tier: This is where the data physically resides, typically in databases.

Why do we need Terraform ?

Configuring AWS resources manually can be very complex and time-consuming. That's why we need Terraform, which is Infrastructure as Code (IAC). Terraform automates the process of configuring applications on instances, making it more efficient and less error-prone. Additionally, we can share the configurations among team members by pushing all the code into a shared repository, further enhancing collaboration and efficiency.

AWS Cloud Architecture

As we can see from the image, we will be deploying the entire application on a VPC. The web tier will be deployed on a public subnet, allowing external traffic to access it. The application and database tiers will be in a private subnet to enhance security.

To enable instances in the private subnet to access the internet, we will deploy a NAT gateway. Additionally, we will configure a bastion host to allow SSH access to instances within the private subnet. While it's generally better to use multiple NAT gateways for redundancy, we'll start with just one for simplicity.

We will also set up an ALB to distribute incoming traffic efficiently. Other essential components include Internet Gateways and Routing tables to ensure proper communication between the different tiers.

Tutorial on deploying three-tier architecture Terraform:

Enough of the theory—let's get our hands dirty!

Prerequisites: Ensure you have the Terraform CLI and AWS CLI installed, along with the necessary IAM access roles.

We’ll be writing our code in HCL, which stands for HashiCorp Configuration Language. This powerful language enables us to define our infrastructure in a declarative way, making it easier to manage and maintain.

Writing Terraform manifest Files

Configure Provider

In Terraform, the "provider" block defines the plugins that allow Terraform to interact with cloud providers. Since we are using AWS as our cloud provider, we will use the AWS provider block. In this block, we need to specify the region where we want to create our resources.

provider "aws" {
  region = "us-east-1"
}

Configure Resources

Configure VPC

First, we need to understand the "resource" block. It's essentially the definition of an infrastructure object. For example, we can create a VPC resource using this block.

Now, let's create a file named vpc.tf.

resource "aws_vpc" "three_tier_app_vpc" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_hostnames = true
  tags = {
    Name = "VPC for three tier architecture"
  }
}

Configure Subnets

Now we will create Public Subnets for web UI instances on different availability zones and connect them to our VPC ID.

## Public Subnet- 1 ##
resource "aws_subnet" "public-web-subnet-1" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.public-web-subnet-1-cidr
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "Public Subnet 1 for web interface"
  }
}
## Public Subnet- 2 ##
resource "aws_subnet" "public-web-subnet-2" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.public-web-subnet-2-cidr
  availability_zone       = "us-east-1b"
  map_public_ip_on_launch = true
  tags = {
    Name = "Public Subnet 2 for web interface"
  }
}

we will create another Private Subnets for backend application instances on different availability zones and connect them to our VPC ID.

### Private Subnet-app 1 ###
resource "aws_subnet" "private-app-subnet-1" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.private-app-subnet-1-cidr
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = false

  tags = {
    Name = "Private Subnet 1 for App Tier"
  }
}
### Private Subnet-app 2 ###
resource "aws_subnet" "private-app-subnet-2" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.private-app-subnet-2-cidr
  availability_zone       = "us-east-1b"
  map_public_ip_on_launch = false

  tags = {
    Name = "Private Subnet 2 for App Tier"
  }
}

then another two private subnets for database instances.

### Private Subnet-db 1 ###
resource "aws_subnet" "private-db-subnet-1" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.private-db-subnet-1-cidr
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = false

  tags = {
    Name = "Private Subnet 1 for Db Tier"
  }
}
### Private Subnet-db 2  ###
resource "aws_subnet" "private-db-subnet-2" {
  vpc_id                  = aws_vpc.three_tier_app_vpc.id
  cidr_block              = var.private-db-subnet-2-cidr
  availability_zone       = "us-east-1b"
  map_public_ip_on_launch = false

  tags = {
    Name = "Private Subnet 2 for Db Tier"
  }
}

Configure Internet Gateway

Now we will configure Internet Gateway So public instances will be accessible to the internet.

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.three_tier_app_vpc.id
  tags = {
    Name = "Internet Gateway"
  }
}

Configure Routing Table

Now we will configure a Routing table with two Routing table associations app instances and two Routing table association for DB instances.

###  Route Table  ###
resource "aws_route_table" "private-route-table" {
  vpc_id = aws_vpc.three_tier_app_vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw.id
  }

  tags = {
    Name = "Private Route Table"
  }
}
###  Route Assoc. for App tier  ###
resource "aws_route_table_association" "nat_route_app_1" {
  subnet_id      = aws_subnet.private-app-subnet-1.id
  route_table_id = aws_route_table.private-route-table.id
}

resource "aws_route_table_association" "nat_route_app_2" {
  subnet_id      = aws_subnet.private-app-subnet-2.id
  route_table_id = aws_route_table.private-route-table.id
}
###  Route Assoc. for DB tier ###
resource "aws_route_table_association" "nat_route_db_1" {
  subnet_id      = aws_subnet.private-db-subnet-1.id
  route_table_id = aws_route_table.private-route-table.id
}

resource "aws_route_table_association" "nat_route_db_2" {
  subnet_id      = aws_subnet.private-db-subnet-2.id
  route_table_id = aws_route_table.private-route-table.id
}

then we will configure the Routing table with two Routing table associations for web instances.

resource "aws_route_table" "public-route-table" {
  vpc_id = aws_vpc.three_tier_app_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = {
    Name = "Public Route Table"
  }
}

resource "aws_route_table_association" "public-subnet-1-route-association" {
  subnet_id      = aws_subnet.public-web-subnet-1.id
  route_table_id = aws_route_table.public-route-table.id
}

resource "aws_route_table_association" "public-subnet-2-route-association" {
  subnet_id      = aws_subnet.public-web-subnet-2.id
  route_table_id = aws_route_table.public-route-table.id
}

Configure NAT Gateways

Now We will configure NAT Gateway and bind an IP by elastic IP.

#   NAT Gateway #
resource "aws_eip" "eip_nat" {
  domain = "vpc"

  tags = {
    Name = "Elastic IP for Nat Gateway"
  }
}

resource "aws_nat_gateway" "nat_gw" {
  allocation_id = aws_eip.eip_nat.id
  subnet_id     = aws_subnet.public-web-subnet-2.id

  tags = {
    Name = "Nat Gateway"
  }
}

Configure Security Groups

We have to configure Security Groups for the application Balancer, Web Instance, Application Instance, and DB Instance

In Application Load Balancer Security Group, We will set ingress rules to enable http/https access on port 80/443 and egress to .

## SG Application Load Balancer ##

resource "aws_security_group" "alb-security-group" {
  name        = "ALB Security Group"
  description = "Enable http/https access on port 80/443"
  vpc_id      = aws_vpc.three_tier_app_vpc.id

  ingress {
    description = "http access"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "https access"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    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 = "ALB Security group"
  }
}

security group for web instance and create ingress rule to enable http/https access on port 80/443 and port 22 for SSH access .

## SG Presentation Tier         ###

resource "aws_security_group" "webserver-security-group" {
  name        = "Web server Security Group"
  description = "Enable http/https access on port 80/443 
                via ALB and ssh via ssh sg"
  vpc_id      = aws_vpc.three_tier_app_vpc.id

  ingress {
    description     = "http access"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = ["${aws_security_group.alb-security-group.id}"]
  }

  ingress {
    description     = "https access"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = ["${aws_security_group.alb-security-group.id}"]
  }
  ingress {
    description     = "ssh access"
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = ["${aws_security_group.ssh-security-group.id}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "Web server Security group"
  }
}

Security Groups for the application tier will act as a base host, allowing app instances in the private subnet to be accessed from an external network. Here you can see inside ingress CIDR block where we are putting our local IP address. So only we can get access

## SG App Tier (Bastion Host) ##

resource "aws_security_group" "ssh-security-group" {
  name        = "SSH Access"
  description = "Enable ssh access on port 22"
  vpc_id      = aws_vpc.three_tier_app_vpc.id

  ingress {
    description = "ssh access"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["${var.ssh-locate}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = -1
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ssh into app tier Security group"
  }
}

This "aws_security_group" resource creates a security group for the database tier, allowing MySQL access on port 3306 only from the web server security group. It also permits all outbound traffic. The security group is tagged for easy identification.

## SG  Database Tier ###
resource "aws_security_group" " database-security-group " {
  name        = " Database server Security Group "
  description = " Enable MYSQL access on port 3306 "
  vpc_id      = aws_vpc.three_tier_app_vpc.id

  ingress {
    description     = " MYSQL access "
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"                                              
    security_groups = ["${aws_security_group.webserver-security-group.id}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = " Database Security group "
  }
}

Configure EC2

This Terraform configuration sets up AWS resources. Defines a data source to fetch the latest Amazon Linux 2 AMI. Creates an RSA key pair and saves the private key locally. Deploys an EC2 instance for the web tier in a public subnet with specific security groups. Deploys another EC2 instance for the app tier in a private subnet, also with appropriate security groups and tagging.

###    Data source   ###
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

resource "aws_key_pair" "terraform_key" {
  key_name           = "tf_key"
  public_key = tls_private_key.rsa.public_key_openssh
}

resource "tls_private_key" "rsa" {
  algorithm = "RSA"
  rsa_bits = 4096
}

resource "local_file" "terraform_key_local_file" {
  content=tls_private_key.rsa.private_key_pem
  filename = "tfkey"
}

###    Ec2 Instance Web Tier   ###
resource "aws_instance" "ec2_public_web" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public-web-subnet-1.id
  vpc_security_group_ids = [aws_security_group.webserver-security-group.id]
  key_name               = aws_key_pair.terraform_key.key_name

  tags = {
    Name = "web-asg"
  }
}

#### EC2 instance APP Tier ###
resource "aws_instance" "ec2_private_app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.private-app-subnet-1.id
  vpc_security_group_ids = [aws_security_group.ssh-security-group.id]
  key_name               = aws_key_pair.terraform_key.key_name
  tags = {
    Name = "app-asg"
  }

}

Configure RDS

This configuration creates an AWS DB subnet group with two private subnets for database instances. It then sets up an AWS RDS MySQL database instance with specified parameters, including storage size, engine version, and instance class.

## database subnet group ##
resource "aws_db_subnet_group" "database-subnet-group" {
  name        = "database subnets"
  subnet_ids  = [aws_subnet.private-db-subnet-1.id, 
                 aws_subnet.private-db-subnet-2.id]
  description = "Subnet group for database instance"

  tags = {
    Name = "Database Subnets"
  }
}

## database instance ##
resource "aws_db_instance" "database-instance" {
  allocated_storage      = 10
  engine                 = "mysql"
  engine_version         = "5.7"
  instance_class         = var.database-instance-class
  db_name                = "sqldb"
  username               = "user123"
  password               = "password"
  parameter_group_name   = "default.mysql5.7"
  skip_final_snapshot    = true
  availability_zone      = "us-east-1b"
  db_subnet_group_name   = aws_db_subnet_group.database-subnet-group.name
  multi_az               = var.multi-az-deployment
  vpc_security_group_ids = [aws_security_group.database-security-group.id]
}

Configure ALB

The configuration sets up an Application Load Balancer (ALB) in AWS with a security group and two public subnets. It creates a target group for the load balancer and attaches an EC2 instance for web interface. An HTTP listener is configured on port 80 to redirect all HTTP traffic to HTTPS on port 443 using a 301 status code.

##   Web app load balancer ##
resource "aws_lb" "application-load-balancer" {
  name                       = "web-external-load-balancer"
  internal                   = false
  load_balancer_type         = "application"
  security_groups            = [aws_security_group.alb-security-group.id]
  subnets                    = [aws_subnet.public-web-subnet-1.id,
                                aws_subnet.public-web-subnet-2.id]
  enable_deletion_protection = false

  tags = {
    Name = "App load balancer"
  }
}

resource "aws_lb_target_group" "alb_target_group" {
  name     = "albtarget"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.three_tier_app_vpc.id
}

resource "aws_lb_target_group_attachment" "web_attachment" {
  target_group_arn = aws_lb_target_group.alb_target_group.arn
  target_id        = aws_instance.ec2_public_web.id
  port             = 80
}

resource "aws_lb_listener" "alb_http_listener" {
  load_balancer_arn = aws_lb.application-load-balancer.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = 443
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

Deploy

Now we will deploy our AWS services using terraform cli.

We have to run this commands at the root of the directory where all our Terraform manifest files (".tf" extension) located.

terraform init

This command initializes working directory and downloads necessary plugins to manage resources. (AWS plugin to manage AWS services)

terraform plan

This is essential before making any adjustments. Based on the current state, it evaluates your configuration and displays what Terraform will create, update, or remove.

terraform apply

This command actually builds or modifies the infrastructure.

terraform destroy

This command will tear down all resources, which is quite opposite of the "apply" command.

Conclusion

If you made it this far, congratulations. Now you can deploy three-tier applications in AWS using Terraform