Infrastructure provisioning for Deploying Three-Tier-Architecture.

Kasturi NithinKasturi Nithin
9 min read

WHY?

There is a way of manually setting up resources and deploying applications on to cloud providers like AWS by following all required security and availability concerns, but there are few disadvantages or drawbacks of doing so.

Drawbacks of setting up resources manually:

  • Errors can occur easily.

  • Time consuming

  • Difficulty in tracking

  • Security risks

  • Disaster recovery problem

HOW?

So, how to eliminate or reduce these kind of drawbacks?

The answer to this question is Infrastructure as Code by using tools like Terraform. One can spin up resources by writing code. Instead of manually clicking and provisioning resources on cloud providers, we can write code to set them up. Read this to learn more.

In this article we will explore how to write code in order to set up resources to deploy an three tier application.

Resources defined using Terraform and Prerequisites to understand better.

  • Availability Zones

  • VPC

  • Subnets [Public and Private]

  • Security Groups

  • Route tables

  • Internet Gateway

  • NAT gateways

  • S3 Bucket

  • IAM Role

Before getting into Resource Definition, let’s understand the below architecture.

It consists of one VPC with CIDR block 10.0.0.0/16 which gives us 2^16 IP addresses. In VPC there are two availability zones [us-east-1a, us-east-1b] for high availability and disaster recovery. In each availability there exists one public subnet and two private subnets, public subnet is defined for WEB-SERVER and one of the two private subnets is for APP-TIER and other is for DATABASE-TIER. External Load Balancer is for routing traffic to any one of the WEB-TIERs available in us-east-1a and us-east-1b. Internal Load Balancer for routing traffic to any one of the APP-TIERs available in us-east-1a and us-east-1b. Internet gateway is defined to allow internet access to WEB-TIER-Subnets, Route table is associated with Internet gateway to allow HTTP and TCP routes to WEB-TIER-Subnets. NAT Gateway is defined to establish connection between WEB-TIER-Subnet[Public] and APP-TIER-Subnet[Private]. Route table is associated with NAT Gateway to allow all routes from WEB-TIER to APP-TIER. An S3 Bucket is created to store code of the application. Finally an IAM Role is defined with specific policies to access the S3 Bucket by resources like ec2.

To get the complete code of this project visit my GitHub.

The entire project is divided into modules for better organization of code.

Module-1

VPC

resource "aws_vpc" "Three-Tier-VPC-Resource" {
  cidr_block = "10.0.0.0/16"
  instance_tenancy = "default"

  enable_dns_hostnames    = true
  enable_dns_support =  true
  tags={
    Name="${var.Project_Name}-VPC"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block of code creates a VPC with 2^16 IP ranges which can be further used by the resources inside the VPC with name Three-Tier-VPC.

Internet Gateway

resource "aws_internet_gateway" "Three-tier-IGW-Resource" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  tags = {
    Name="${var.Project_Name}-IGW"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This resource definition creates Internet Gateway with name Three-Tier-IGW in the VPC which was defined earlier[Three-Tier-VPC].

Route table for Internet Gateway

resource "aws_route_table" "Three-Tier-Public-RT-Resource" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  route  {
    cidr_block="0.0.0.0/0"
    gateway_id=aws_internet_gateway.Three-tier-IGW-Resource.id
  }
  tags = {
    Name="Public-RT"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

These lines of code creates an Route table with name Public-RT in the VPC[Three-Tier-VPC], it has one route which allows internet from Internet gateway[Three-Tier-IGW] to which ever resource under this route table.

Get Availability Zones

data "aws_availability_zones" "available_zones" {

}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

These lines fetch you the total number of availability zones in the region and stores in data named available_zones.

Web-Tier-Public-Subnet-1

resource "aws_subnet" "Web_tier-Subnet-1-Pub" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.Web-Tier-1a-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[0]
  map_public_ip_on_launch = true
  tags = {
    Name="Web-Tier-Subnet-1a-Pub"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Public subnet with name Web-Tier-Subnet-1a-Pub inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.1.0/24, in one of the available_zones i.e us-east-1a.

Web-Tier-Public-Subnet-2

resource "aws_subnet" "Web_tier-Subnet-2-Pub" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.Web-Tier-2a-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[1]
  map_public_ip_on_launch = true
  tags = {
    Name="Web-Tier-Subnet-1b-Pub"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Public subnet with name Web-Tier-Subnet-1b-Pub inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.4.0/24, in one of the available_zones i.e us-east-1b.

App-Tier-Subnet-1-Private

resource "aws_subnet" "App_tier-Subnet-1-Private" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.App-Tier-1a-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[0]
  map_public_ip_on_launch = false
  tags = {
    Name="App-Tier-Subnet-1a-Pri"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Private subnet with name App-Tier-Subnet-1a-Pri inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.2.0/24, in one of the available_zones i.e us-east-1a.

App-Tier-Subnet-2-Private

resource "aws_subnet" "App_tier-Subnet-2-Private" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.App-Tier-1b-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[1]
  map_public_ip_on_launch = false
  tags = {
    Name="App-Tier-Subnet-1b-Pri"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Private subnet with name App-Tier-Subnet-1b-Pri inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.5.0/24, in one of the available_zones i.e us-east-1b.

DB-Tier-Subnet-1-Private

resource "aws_subnet" "DB_tier-Subnet-1-Private" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.DB-Tier-1a-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[0]
  map_public_ip_on_launch = false
  tags = {
    Name="DB-Tier-Subnet-1a-Pri"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Private subnet with name DB-Tier-Subnet-1a-Pri inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.3.0/24, in one of the available_zones i.e us-east-1a.

DB-Tier-Subnet-2-Private

resource "aws_subnet" "DB_tier-Subnet-2-Private" {
  vpc_id = aws_vpc.Three-Tier-VPC-Resource.id
  cidr_block = var.DB-Tier-1b-cidr
  availability_zone = data.aws_availability_zones.available_zones.names[1]
  map_public_ip_on_launch = false
  tags = {
    Name="DB-Tier-Subnet-1b-Pri"
  }
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block creates a Private subnet with name DB-Tier-Subnet-1b-Pri inside the VPC[Three-Tier-VPC], within the CIDR block 10.0.0.6.0/24, in one of the available_zones i.e us-east-1a.

Assign public subnets to public route table

resource "aws_route_table_association" "Web_tier-Subnet-1-Pub-RTA" {
  subnet_id = aws_subnet.Web_tier-Subnet-1-Pub.id
  route_table_id = aws_route_table.Three-Tier-Public-RT-Resource.id
}
resource "aws_route_table_association" "Web_tier-Subnet-2-Pub-RTA" {
  subnet_id = aws_subnet.Web_tier-Subnet-2-Pub.id
  route_table_id = aws_route_table.Three-Tier-Public-RT-Resource.id
}
#This code is inside the repository InfraAsCode/modules/vpc/main.tf

This block of code associates the Route table [Public-RT] with the public subnets namely Web-Tier-Subnet-1a-Pub and Web-Tier-Subnet-1b-Pub for internet access.

Module-2-Security Groups

Security Group[External] for Web-Tier-Instances

resource "aws_security_group" "alb_sg" {
  name = "ALB security group"
  description = "enable http/https access on port 80/443"
  vpc_id = var.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-sg"
  }

}
#This code is inside the repository InfraAsCode/modules/securitygroup/main.tf

This code snippet creates security group with name ALB-sg for Web-TIer-Servers that allows 80 and 443 ports inside the server and every port to outside world.

Security Group[Internal] for App-Tier-Instances

resource "aws_security_group" "internal-sg" {
  name = "Internal Security group"
  description = "Allow access from alb sg"
  vpc_id = var.vpc_id
  ingress{
    description = "https access"
    from_port = 80
    to_port = 80
    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="Internal-sg"
  }
}
#This code is inside the repository InfraAsCode/modules/securitygroup/main.tf

This code snippet creates a security group named Internal-sg for App-Tier servers. It allows traffic on port 80 only from the ALB-sg, which was defined earlier, and permits all outbound traffic to the outside world.

Module-3-NAT

Elastic IP for NAT Gateway in us-east-1a

resource "aws_eip" "EIP_for_NAT1" {
  vpc = true
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This bit of code creates an elastic IP, which has to be attached to the NAT gateway.

NAT Gateway for Web-Tier1

resource "aws_nat_gateway" "NAT_for_Web_Tier_1" {
  subnet_id = var.Web_Tier_Subnet1_id
  allocation_id = aws_eip.EIP_for_NAT1.id
  tags = {
    Name = "NAT_for_Web_Tier_1"
  }

}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This code defines a NAT Gateway with name NAT_for_Web_Tier_1 for the Web-Tier-Subnet-1-Public and associates an elastic IP to it.

Route Table for NAT_for_Web_Tier_1

resource "aws_route_table" "RT_for_Web_Tier_1" {
  vpc_id = var.vpc_id
  route{
    cidr_block ="0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.NAT_for_Web_Tier_1.id
  }
  tags = {
    Name="RT_for_Web_Tier_1"
  }
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

It creates an Route Table with name RT_for_Web_Tier_1 in the same VPC where all the other resources reside, It allows all the routes from internet to NAT_for_Web_Tier_1.

Route Table Association to App-Tier-Subnet1

resource "aws_route_table_association" "RTA_for_RT_Web_Tier_1" {
  subnet_id = var.App_Tier_Subnet1_id
  route_table_id = aws_route_table.RT_for_Web_Tier_1.id
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This code snippet associates the route table RT_for_Web_Tier_1 with App_Tier_Subnet1_Private.

Elastic IP for NAT Gateway in us-east-1b

resource "aws_eip" "EIP_for_NAT2" {
  vpc = true
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This bit of code creates an elastic IP, which has to be attached to the NAT gateway.

NAT Gateway for Web-Tier2

resource "aws_nat_gateway" "NAT_for_Web_Tier_2" {
  subnet_id = var.Web_Tier_Subnet2_id
  allocation_id = aws_eip.EIP_for_NAT2.id
  tags = {
    Name = "NAT_for_Web_Tier_2"
  }

}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This code defines a NAT Gateway with name NAT_for_Web_Tier_2 for the Web-Tier-Subnet-2-Public and associates an elastic IP to it.

Route Table for NAT_for_Web_Tier_2

resource "aws_route_table" "RT_for_Web_Tier_2" {
  vpc_id = var.vpc_id
  route{
    cidr_block ="0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.NAT_for_Web_Tier_2.id
  }
  tags = {
    Name="RT_for_Web_Tier_2"
  }
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

It creates an Route Table with name RT_for_Web_Tier_2 in the same VPC where all the other resources reside, It allows all the routes from internet to NAT_for_Web_Tier_2.

Route Table Association to App-Tier-Subnet1

resource "aws_route_table_association" "RTA_for_RT_Web_Tier_2" {
  subnet_id = var.App_Tier_Subnet2_id
  route_table_id = aws_route_table.RT_for_Web_Tier_2.id
}
#This code is inside the repository InfraAsCode/modules/nat/main.tf

This code snippet associates the route table RT_for_Web_Tier_2 with App_Tier_Subnet2_Private.

Module-4-S3-Bucket

resource "aws_s3_bucket" "Bucket_for_code" {
  bucket = "Bucket_for_code"
  tags={
    Name="Bucket_for_code"
  }
}

Here’s how you create an S3 bucket with name Bucket_for_code to store the code of the application.

Module-5-IAM

IAM

resource "aws_iam_role" "RoleForBucketAccess" {
  name = "RoleForBucketAccess"

  # Terraform's "jsonencode" function converts a
  # Terraform expression result to valid JSON syntax.
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      },
    ]
  })

  tags = {
    tag-key = "RoleForBucketAccess"
  }
}
#This code is inside the repository InfraAsCode/modules/iam/main.tf

This code snippet creates an IAM Role with name RoleForBucketAccess, so that it can be used by other resources like ec2 to get limited access to the bucket.

IAM Policies

resource "aws_iam_role_policy_attachment" "ssm_policy_attachment" {
  role       = aws_iam_role.RoleForBucketAccess.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy_attachment" "s3_read_only_attachement" {
  role       = aws_iam_role.RoleForBucketAccess.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
#This code is inside the repository InfraAsCode/modules/iam/main.tf

This code attaches two policies, one is SSMManagedInstanceCore and other is S3ReadOnlyAccess.

  • SSMManagedInstanceCore- It gives ec2 instance a permission to work with Amazon Systems Manager.

  • S3ReadOnlyAccess- It gives read-only access to all S3 buckets in the AWS account to the resource the role is assigned to.

All the resources depicted in the architecture diagram are created and connected to each other in the way required, we can utilize them to deploy our application with high security, availability, recovery.

Note: If you have followed along, please don’t forget to delete the created resources using ‘terraform destroy‘, also check for elastic IPs and delete if found.

0
Subscribe to my newsletter

Read articles from Kasturi Nithin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kasturi Nithin
Kasturi Nithin

Exploring something new.