Building a 3-tier AWS Network Infrastructure using Terraform Modules

Building a 3-tier AWS Network Infrastructure using Terraform Modules

Introduction

In today’s fast-paced digital landscape, the ability to deploy scalable and highly available applications is paramount for businesses aiming to stay competitive and meet ever-growing user demands. Amazon Web Services(AWS) has emerged as a leading choice for hosting and managing applications at scale among the various cloud computing platforms available.

With its extensive suite of services, robust infrastructure, and global reach, AWS provides a solid foundation for developers and organizations seeking to deploy applications that can handle increasing traffic, ensure uninterrupted performance, and adapt to evolving user needs.

Why use Terraform Modules?

For this project, we resulted to using Terraform Modules because they allow you to break down your infrastructure code into smaller, reusable components. You can encapsulate a set of resources, configurations, and logic related to a specific functionality or application into a module. This modularity makes the codebase cleaner, easier to maintain and promotes code reuse across different projects.

Prerequisites

  • AWS CLI installed and AWS credentials installed

  • Visual Studio Code

  • Terraform installed

  • Proper Understanding of AWS Networking (VPC, Subnets, Route Tables, Security Groups, etc)

Initializing a Terraform project

Creating a VPC

First, we create an empty project folder, and within the folder, you add a “modules” folder. This is the folder that you will reference for your project. Within the “modules” folder, create a new file called “main.tf”. We’d start by creating the VPC. To create the VPC, enter the following code on the main.tf

resource "aws_vpc" "vpc" {
    cidr_block = var.vpc_cidr
    instance_tenancy = "default"
    enable_dns_hostnames = true

    tags = {
        Name = "${var.project_name}-vpc"
    }
}

The code above creates the VPC in the module where the CIDR block is defined as a variable. We are required to set the instance_tenancy to “default”, and set “enable_dns_hostnames” to true. For proper identification, we are required to tag our VPC, so we’d tag it according to the project name on the variables.

Creating an Internet Gateway

Next, we’d add the Internet Gateway. On the AWS infrastructure, the internet gateway is a horizontally scaled, redundant, and highly available VPC component that allows communication between your VPC and the internet. To create the Internet gateway, enter the following code on the main.tf

resource "aws_internet_gateway" "internet_gateway" {
    vpc_id = aws_vpc.vpc.id

    tags = {
        Name = "${var.project_name}-igw"
    }
}

Here, we’d specify the VPC we want to attach the internet gateway to. This is the custom VPC in our previous task.

Next, we need to get all the availability zones in the infrastructure to evenly distribute our architecture, and make it highly available. We’d use the following command.

data "aws_availability_zones" "available_zones" {}

Creating the Subnets

For the next step, we'd be creating subnets as part of our infrastructure network. A subnet is a range of IP addresses that work with your VPC. Each subnet must reside entirely within one Availability Zone because by launching AWS resources in separate Availability Zones, you can protect your applications from the failure of a single Availability Zone.

For this project, we will create only two types of Public Subnets per availability zone. The Public subnet, and the private Subnets. the Public Subnets provide direct access to the internet gateway and to the public internet.

On the other hand, resources in the private subnet do not have a direct route to an internet gateway.

To set up the subnets using Terraform, use the following code.

resource "aws_subnet" "public_subnet_az1" {
    vpc_id = aws_vpc.vpc.id
    cidr_block = var.public_subnet_az1_cidr
    availability_zone = data.aws_availability_zones.available_zones.names[0]
    map_public_ip_on_launch = true

    tags = {
        Name = "public subnet az1"
    }
}

# create route table and add public route
resource "aws_route_table" "public_route_table" {
  vpc_id       = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.internet_gateway.id
  }

  tags       = {
    Name     = "public route table"
  }
}

# associate public subnet az1 to "public route table"
resource "aws_route_table_association" "public_subnet_az1_route_table_association" {
  subnet_id           = aws_subnet.public_subnet_az1.id
  route_table_id      = aws_route_table.public_route_table.id
}

# associate public subnet az2 to "public route table"
resource "aws_route_table_association" "public_subnet_az2_route_table_association" {
  subnet_id           = aws_subnet.public_subnet_az2.id
  route_table_id      = aws_route_table.public_route_table.id
}

The code above specified the public subnets and the route tables and association to movie traffic and access the public internet.

Creating a variables.tf folder.

The next step for this project is to create the variables files for the modules. This is were the variables are called out for your module. The code is written as follows

variable "region" {}
variable "project_name" {}
variable "vpc_cidr" {}
variable "public_subnet_az1_cidr" {}
variable "public_subnet_az2_cidr" {}
variable "private_app_subnet_az1_cidr" {}
variable "private_app_subnet_az2_cidr" {}
variable "private_data_subnet_az1_cidr" {}
variable "private_data_subnet_az2_cidr" {}

Creating the outputs.tf for the module

Next, we will create the outputs we'd want to see after applying the code to create the infrastructure.

output "region" {
    value = var.region
}

output "project_name" {
    value = var.project_name
}

output "vpc_id" {
    value = aws_vpc.vpc.id
}

output "public_subnet_az1_id" {
    value = aws_subnet.public_subnet_az1.id
}

output "public_subnet_az2_id" {
    value = aws_subnet.public_subnet_az2.id
}

output "private_app_subnet_az1_cidr" {
    value = aws_subnet.private_app_subnet_az1.id
}

output "private_app_subnet_az2_cidr" {
    value = aws_subnet.private_app_subnet_az2.id
}

output "private_data_subnet_az1_cidr" {
    value = aws_subnet.private_data_subnet_az1.id
}

output "private_data_subnet_az2_cidr" {
    value = aws_subnet.private_data_subnet_az2.id
}

output "internet_gateway" {
    value = aws_internet_gateway.internet_gateway.id
}

Building up the project file

Now that we have clearly defined the modules, we need to do is clearly start creating the main project file. For this project, we'd name it "Jupiter". Create the "Jupiter" project folder, and within the project, create a 'backend.tf' file. Input the following code in the file.

# store the terraform state file in s3
terraform {
  backend "s3" {
    bucket = "ezekiel-terra-state"
    key    = "ezekiel-terra-state.tfstate"
    region = "us-east-1"
  }
}

Here, we are storing the terraform state of the infrastructure on an s3 bucket. This will come in handy when working on an infrastructure with multiple engineers.

Next, we'd create the 'main.tf' file where we'd set our AWS provider, set the preferred regions, and import the modules using the following command.

provider "aws" {
  region  = var.region
}

module "vpc" {
  source                       = "../modules/vpc"
  region                       = var.region
  project_name                 = var.project_name
  vpc_cidr                     = var.vpc_cidr
  public_subnet_az1_cidr       = var.public_subnet_az1_cidr
  public_subnet_az2_cidr       = var.public_subnet_az2_cidr
  private_app_subnet_az1_cidr  = var.private_app_subnet_az1_cidr
  private_app_subnet_az2_cidr  = var.private_app_subnet_az2_cidr
  private_data_subnet_az1_cidr = var.private_data_subnet_az1_cidr
  private_data_subnet_az2_cidr = var.private_data_subnet_az2_cidr
}

Creating variables.tf and terraform.tfvars for the project

For the variables.tf, we'd input the following code.

variable "region" {}
variable "project_name" {}
variable "vpc_cidr" {}
variable "public_subnet_az1_cidr" {}
variable "public_subnet_az2_cidr" {}
variable "private_app_subnet_az1_cidr" {}
variable "private_app_subnet_az2_cidr" {}
variable "private_data_subnet_az1_cidr" {}
variable "private_data_subnet_az2_cidr" {}

on the terraform.tfvars we define the variables properly using the following code.

region                       = "us-east-1"
project_name                 = "jupiter"
vpc_cidr                     = "10.0.0.0/16"
public_subnet_az1_cidr       = "10.0.0.0/24"
public_subnet_az2_cidr       = "10.0.1.0/24"
private_app_subnet_az1_cidr  = "10.0.2.0/24"
private_app_subnet_az2_cidr  = "10.0.3.0/24"
private_data_subnet_az1_cidr = "10.0.4.0/24"
private_data_subnet_az2_cidr = "10.0.5.0/24"

Running the Terraform code

To deploy your infrastructure, first run

terraform init

This command will initialize the terraform state in your project, and once you deploy your infrastructure, it stores the state in your s3 bucket.

To go through your code organization, simply run. "terraform plan" This command will show you what you need to apply, and apply it properly.

terraform apply --auto-approve

Once the infrastructure is applied, it will look like this

Conclusion

In conclusion, building a 3-tier AWS network infrastructure using Terraform modules offers an efficient, scalable, and manageable solution for businesses seeking to deploy robust and reliable applications in the cloud.

Throughout this article, we have explored the benefits and importance of adopting a modular approach to network architecture, leveraging the power of Terraform to create a flexible and automated environment.