Creating a Virtual Private Cloud (VPC) in Amazon Web Service (AWS) is a hassle if done through the AWS console or CLI. By utilising Terraform, we can create a VPC in seconds.

Before we start off with terraform to create our VPC, let's look into AWS VPC deeper (https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html) and understand what is it.

What is a VPC?

A VPC is an isolated virtual network in AWS that can allow you to launch your AWS resources into. It can contains different subnets which is up to you to define. In this scenario, we will create a VPC which contains both the private subnets and public subnets.

The private subnets (think of LAN) will have a default route in the route table to route the traffic out to the internet via the NAT gateway (https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html), while the public subnets will have a default route in the route table to route the traffic out to the internet via the internet gateway (https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html).

To ensure high-availability for our AWS resources, we will create a subnet in each respective availability-zone (AZ) for the chosen AWS region. An AZ is a AWS datacenter in the region. Thus, having a subnet in each of the AZ ensure that we can load-balance our resources across the different AZ in the region, allowing us to mitigate any possible risk of downtime during AZ failure.

Why not the default VPC?

The default VPC in AWS defaults to 172.31.0.0/16 and contains only public subnets. For every project deployed in AWS, it's advisable to create your own VPC to ensure your own IP addressing and also ensure the presence of both private and public subnets to be able to segregate your resources in the proper network. For example, your database should be created in the private subnet instead of the public subnets.

Your custom VPC with it's own IP addressing also allows you the flexibility in peering with other VPCs in the future if required.

How to use Terraform to create VPC?

The usual with every new terraform scripts, we will look for the respective provider in the terraform registry. For this particular case, we will be using the Hashicorp AWS provider (https://registry.terraform.io/providers/hashicorp/aws/latest).

We will start by adding our required providers in the a main.tf file.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  alias = "sg"
  region = "ap-southeast-1"
}

We will then need to add in the respective resources required for our VPC by referring to the documentations for our terraform provider (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc). We can create a sg.tf file and pasted the below content inside.

resource "aws_vpc" "sg" {
  provider = aws.sg

  cidr_block       = local.sg_vpc_cidr
  instance_tenancy = "default"
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    Name = "SG-VPC"
    Project = local.projecttag
  }
}

resource "aws_subnet" "private_1a_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_private_subnet_cidr_1a
  availability_zone = "ap-southeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "Private Subnet 1a"
    Project = local.projecttag
  }
}

resource "aws_subnet" "private_1b_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_private_subnet_cidr_1b
  availability_zone = "ap-southeast-1b"
  map_public_ip_on_launch = true

  tags = {
    Name = "Private Subnet 1b"
    Project = local.projecttag
  }
}

resource "aws_subnet" "private_1c_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_private_subnet_cidr_1c
  availability_zone = "ap-southeast-1c"
  map_public_ip_on_launch = true

  tags = {
    Name = "Private Subnet 1c"
    Project = local.projecttag
  }
}

resource "aws_subnet" "public_1a_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_public_subnet_cidr_1a
  availability_zone = "ap-southeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "Public Subnet 1a"
    Project = local.projecttag
  }
}

resource "aws_subnet" "public_1b_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_public_subnet_cidr_1b
  availability_zone = "ap-southeast-1b"
  map_public_ip_on_launch = true

  tags = {
    Name = "Public Subnet 1b"
    Project = local.projecttag
  }
}

resource "aws_subnet" "public_1c_sg" {
  provider = aws.sg

  vpc_id     = aws_vpc.sg.id
  cidr_block = local.sg_public_subnet_cidr_1c
  availability_zone = "ap-southeast-1c"
  map_public_ip_on_launch = true

  tags = {
    Name = "Public Subnet 1c"
    Project = local.projecttag
  }
}

# Private Route Table SG

resource "aws_route_table" "private_subnet_sg" {
  provider = aws.sg

  vpc_id = aws_vpc.sg.id
  
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.natgw_sg.id
  }

  tags = {
    Name = "Private RT"
    Project = local.projecttag
  }
}

# Public Route Table SG

resource "aws_route_table" "public_subnet_sg" {
  provider = aws.sg

  vpc_id = aws_vpc.sg.id

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

  tags = {
    Name = "Public RT"
    Project = local.projecttag
  }
}

# Route Table Association

resource "aws_route_table_association" "private_routetable_sg_a" {
  provider = aws.sg

  route_table_id = aws_route_table.private_subnet_sg.id
  subnet_id = aws_subnet.private_1a_sg.id
}

resource "aws_route_table_association" "private_routetable_sg_b" {
  provider = aws.sg

  route_table_id = aws_route_table.private_subnet_sg.id
  subnet_id = aws_subnet.private_1b_sg.id
}

resource "aws_route_table_association" "private_routetable_sg_c" {
  provider = aws.sg

  route_table_id = aws_route_table.private_subnet_sg.id
  subnet_id = aws_subnet.private_1c_sg.id
}

resource "aws_route_table_association" "public_routetable_sg_a" {
  provider = aws.sg

  route_table_id = aws_route_table.public_subnet_sg.id
  subnet_id = aws_subnet.public_1a_sg.id
}

resource "aws_route_table_association" "public_routetable_sg_b" {
  provider = aws.sg

  route_table_id = aws_route_table.public_subnet_sg.id
  subnet_id = aws_subnet.public_1b_sg.id
}

resource "aws_route_table_association" "public_routetable_sg_c" {
  provider = aws.sg

  route_table_id = aws_route_table.public_subnet_sg.id
  subnet_id = aws_subnet.public_1c_sg.id
}

# EIP Nat Gateway SG

resource "aws_eip" "eip_ngw_sg" {
  provider = aws.sg

  vpc      = true

  tags = {
    Name = "SG NAT Gateway EIP"
    Project = local.projecttag
  }
}

# Nat Gateway SG

resource "aws_nat_gateway" "natgw_sg" {
  provider = aws.sg

  allocation_id = aws_eip.eip_ngw_sg.id
  subnet_id     = aws_subnet.public_1a_sg.id

  tags = {
    Name = "SG NAT Gateway"
    Project = local.projecttag
  }
}

# Internet Gateway - SG

resource "aws_internet_gateway" "igw_sg" {
  provider = aws.sg

  vpc_id = aws_vpc.sg.id

  tags = {
    Name = "SG Internet Gateway"
    Project = local.projecttag
  }
}

If you look closely at the snippet above, you will realised that there are some variables being used in the format local.<variable name>. These will be declared in the next file named variables.tf.

locals {
  projecttag                = "SG"

  # SG VPC
  sg_vpc_cidr               = "10.29.0.0/16"
  sg_private_subnet_cidr_1a = "10.29.0.0/20"
  sg_private_subnet_cidr_1b = "10.29.16.0/20"
  sg_private_subnet_cidr_1c = "10.29.32.0/20"
  sg_public_subnet_cidr_1a = "10.29.112.0/20"
  sg_public_subnet_cidr_1b = "10.29.128.0/20"
  sg_public_subnet_cidr_1c = "10.29.144.0/20"
}

Once you have these 3 files created, you can start the terraform script by running the following commands.

First, initialise the Terraform modules with the command below.

terraform init

Next, plan the resources that you're going to create.

terraform plan

Once you're satisfied with the planned output, apply the script.

terraform apply

Terraform will ask you to confirm by typing yes in the prompt. It's a good practice to double check the output to make sure it's the correct resources that you wish to create before confirming.

Finally, wait for Terraform to do it's magic and you will have a brand new VPC in your AWS account!

Check out the whole process in the video below.

If you wish to add/modify any resources in your VPC after creation, modify or create the respective tf files and repeat the above steps less the init command. Terraform will refer to the state in the *.tfstate file in the directory and apply the modification accordingly.

How to destroy the VPC?

Assuming you wish to destroy this VPC as it's no longer needed anymore, you can execute the below command to destroy the VPC.

terraform destroy

Terraform will seek your confirmation before it will proceed with the destruction of the VPC.

You can refer to the GitHub repository here for the terraform script in this article (https://github.com/alexlogy/terraform-aws-vpc).