Creating a Production Grade VPC in AWS using terraform

Saumik Satapathy
7 min readJul 19, 2020

What is VPC?
A VPC is a private network within AWS. In Layman term, we can say it’s a private datacenter inside AWS platform. It can be configured as public, private or mixture. It’s region-specific i.e. a single VPC can’t place across regions. We can connect our private data centres and corporate networks to VPC.
* It’s isolated from other VPC by default.
* A VPC can have maximum CIDR as /16(65,536 IPs) and minimum /28(16 IPs).

What is custom VPC?
A custom VPC can be designed and configured as per requirement. We can provide IP Ranges, create multiple subnets, provision gateways and networking as well as design and implement security.

Let’s create a custom VPC with 3 public and 3 private subnets, A Internet Gateway, and few other components like Route Table, NAT Gateway etc.

The main purpose of this setup is that our private resources can get Internet connectivity but not vice-versa. Our private resources remain isolated from the direct reach of the internet and hackers.

To begin the configuration we need to create a separate folder in our wor station which will isolate our resources from conflict with others.

$ mkdir vpc
$ cd vpc

First, we have to provide terraform which service is going to use this. for this, we need to create a vpc.tf file. We don’t need to hardcode the region so we can use the same setup for other regions as well.

provider "aws" {
region = var.region
}
resource "aws_vpc" "production" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = "Production"
}
}

Now, we have to declare the region, vpc CIDR in the file which will contain all the variable details. We will name that as vars.tf for our clear understanding.

variable "region" {
default = "ap-south-1"
description = "AWS Region"
}
variable "vpc_cidr" {
description = "VPC CIDR"
}

It’s time to create one private and one public subnet in each Availability Zone. We will name the file as subnet.tf. but before creating subnet.tf we need to update the vars.tf file to use in the subnet.tf file. The updated vars.tf file will be like the below,

variable "region" {
default = "ap-south-1"
description = "AWS Region"
}
variable "vpc_cidr" {
description = "vpc CIDR Block"
}
variable "public_subnet_1_cidr" {
description = "Public Subnet 1 CIDR"
}
variable "public_subnet_2_cidr" {
description = "Public Subnet 2 CIDR"
}
variable "public_subnet_3_cidr" {
description = "Public Subnet 3 CIDR"
}
variable "private_subnet_1_cidr" {
description = "Private Subnet 1 CIDR"
}
variable "private_subnet_2_cidr" {
description = "Private Subnet 2 CIDR"
}
variable "private_subnet_3_cidr" {
description = "Private Subnet 3 CIDR"
}

After declaring the required variables in the vars.tf file we will now move on to create the subents.tf

resource "aws_subnet" "public-subnet-1" {
cidr_block = var.public_subnet_1_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1a"
tags = {
Name = "Public-Subent-1"
}
}
resource "aws_subnet" "public-subnet-2" {
cidr_block = var.public_subnet_2_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1b"
tags = {
Name = "Public-Subent-2"
}
}
resource "aws_subnet" "public-subnet-3" {
cidr_block = var.public_subnet_3_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1c"
tags = {
Name = "Public-Subent-3"
}
}
resource "aws_subnet" "private-subnet-1" {
cidr_block = var.private_subnet_1_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1a"
tags = {
Name = "Private-Subent-1"
}
}
resource "aws_subnet" "private-subnet-2" {
cidr_block = var.private_subnet_2_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1b"
tags = {
Name = "Private-Subent-2"
}
}
resource "aws_subnet" "private-subnet-3" {
cidr_block = var.private_subnet_3_cidr
vpc_id = aws_vpc.production.id
availability_zone = "ap-south-1c"
tags = {
Name = "Private-Subent-3"
}
}

After creating the subnet it’s time to create the Route Table and associate the respective subnets to it. We will give the name of the file as rt.tf.

resource "aws_route_table" "public-route-table" {
vpc_id = aws_vpc.production.id
tags = {
Name = "Public-Route-Table"
}
}
resource "aws_route_table" "private-route-table" {
vpc_id = aws_vpc.production.id
tags = {
Name = "Private-Route-Table"
}
}
resource "aws_route_table_association" "public-subnet-1-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-1.id
}
resource "aws_route_table_association" "public-subnet-2-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-2.id
}
resource "aws_route_table_association" "public-subnet-3-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-3.id
}
resource "aws_route_table_association" "private-subnet-1-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-1.id
}
resource "aws_route_table_association" "private-subnet-2-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-2.id
}
resource "aws_route_table_association" "private-subnet-3-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-3.id
}

To give the private instances internet connectivity we can opt either NAT Instance or NAT Gateway. Now a Days NAT Instance is an outdated concept. If you compare NAT Gateway over NAT Instance we can found that,
* NAT Gateway can have a bandwidth of 45 Gbps whereas inNAT Instance it depends upon the instance family. Lower the type lower the speed.
* The main advantage of NAT Gateway over NAT Instance is that aNATGateway is managed by AWS. we do not need to perform any maintenance. Whereas in NAT Instance we have to manage everything, for example, by installing software updates or operating system patches on the instance.

For this demo let’s go with NAT Gateway. To create NAT Gateway we also need to create an Elastic IP because NAT Gateway internally uses Elastic IP and we have to prove that in the terraform file. So before creating the terraform file for NAT Gateway we will create the terraform file for Elastic IP. We can give it a name as eip.tf. We can also assign a private IP to EIP so it won’t pick up any random private IP.

resource "aws_eip" "eip" {
vpc = true
associate_with_private_ip = "192.168.0.5"
tags = {
Name = "Production-EIP"
}
}

After created the terraform file for Elastic IP let’s jump over and create the terraform file for NAT Gateway. We can give the file a name as nat.tf.

resource "aws_nat_gateway" "nat-gw" {
allocation_id = aws_eip.eip.id
subnet_id = aws_subnet.public-subnet-1.id
tags = {
Name = "Production-NAT-GW"
}
depends_on = [aws_eip.eip]
}

To get internet inside the VPC we have to create an Internet Gateway and update the Route table as well. To create an Internet Gateway we will now create a terraform file named igw.tf. The content of the file is as below,

resource "aws_internet_gateway" "production-igw" {
vpc_id = aws_vpc.production.id
tags = {
Name = "Production-IGW"
}
}

After the Internet Gateway has been created we need to update the Route Table to allow internet inside the subnets. The updated Route Table file i.e rt.tf will look as follows,

resource "aws_route_table" "public-route-table" {
vpc_id = aws_vpc.production.id
tags = {
Name = "Public-Route-Table"
}
}
resource "aws_route_table" "private-route-table" {
vpc_id = aws_vpc.production.id
tags = {
Name = "Private-Route-Table"
}
}
resource "aws_route_table_association" "public-subnet-1-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-1.id
}
resource "aws_route_table_association" "public-subnet-2-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-2.id
}
resource "aws_route_table_association" "public-subnet-3-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-3.id
}
resource "aws_route_table_association" "private-subnet-1-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-1.id
}
resource "aws_route_table_association" "private-subnet-2-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-2.id
}
resource "aws_route_table_association" "private-subnet-3-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-3.id
}
resource "aws_route" "public-internet-gw-route" { route_table_id = aws_route_table.public-route-table.id
gateway_id = aws_internet_gateway.production-igw.id
destination_cidr_block = "0.0.0.0/0"
}
resource "aws_route" "nat-gw-route" {
route_table_id = aws_route_table.private-route-table.id
nat_gateway_id = aws_nat_gateway.nat-gw.id
destination_cidr_block = "0.0.0.0/0"
}

Our infrastructure setup is completed let’s create an outputs.tf file to get the output in the console which resources have been created.

output "vpc_id" {
value = aws_vpc.production.id
}
output "vpc_cidr_block" {
value = aws_vpc.production.cidr_block
}
output "public_subnet_1_id" {
value = aws_subnet.public-subnet-1.id
}
output "public_subnet_2_id" {
value = aws_subnet.public-subnet-2.id
}
output "public_subnet_3_id" {
value = aws_subnet.public-subnet-3.id
}
output "private_subnet_1_id" {
value = aws_subnet.private-subnet-1.id
}
output "private_subnet_2_id" {
value = aws_subnet.private-subnet-2.id
}
output "private_subnet_3_id" {
value = aws_subnet.private-subnet-3.id
}

The final step is to create a tfvars file which will contain all the missing variables as well as it easy to modify the value later in one file rather than changing the whole infrastructure. The production.tfvars file looks something like the below,

vpc_cidr              = "192.168.0.0/16"
public_subnet_1_cidr = "192.168.1.0/24"
public_subnet_2_cidr = "192.168.2.0/24"
public_subnet_3_cidr = "192.168.3.0/24"
private_subnet_1_cidr = "192.168.4.0/24"
private_subnet_2_cidr = "192.168.5.0/24"
private_subnet_3_cidr = "192.168.6.0/24"

Let’s apply the code by providing the variable and see what going to happen.

$ terraform init
$ terraform plan --var-file="production.tfvars"
$ terraform apply --var-file="production.tfvars"

If everything is written correctly then the terraform will create a New VPC with three subnets each in Private and Public. An Internet Gateway with the update Routes in Route Table. A NAT Gateway with an EIP.

The reason behind passing the variable values in a separate file i.e. .tfvars is that we can change the value dynamically. It’s easy to change the value at one place rather than changing at every file. One more advantage also if you don’t pass the variable file while applying the terraform it will ask you to enter the value at run time. We can also provide the values on the go.

without passing the ‘tfvars’ file

Note: If you’re running the terraform without tfvars file, then you have to comment or delete the associate_with_private_ip line in eip.tf file so it won’t throw any error.

All the codes are available in Github. Repo URL: https://github.com/saumik8763/terraform-vpc

--

--

Saumik Satapathy

A passionate software Engineer with good hands on experience in the field of DevOps/SRE. Love to share knowledge and intersted to learn from others.