Okey Ebere
5 min readOct 17, 2023

CREATING A CUSTOM VPC ON AWS USING TERRAFORM

In the age of cloud migration, establishing resilient and secure Virtual Private Cloud (VPC) networks is paramount. AWS offers a robust VPC service, enabling users to create virtual networks in the cloud. However, configuring and managing a VPC can be complex and time-consuming. Terraform plays a key role. Terraform is an open-source infrastructure as a code tool that steps in to simplify and automate the creation and management of AWS resources.
This article will explore how Terraform streamlines setting up a custom VPC on AWS.

Prerequisites

  • AWS Cli: Installed and Configured
  • Terraform
  • AWS Account

GitHub Repository Link

Architectural diagram

The diagram above illustrates a fundamental VPC design that incorporates the following components:

  1. A Virtual Private Cloud with the specified CIDR block.
  2. Two public subnets, with each subnet associated with a specific availability zone.
  3. Two private subnets, with each subnet associated with a specific availability zone.
  4. Route Table Associations for Public Subnets
  5. Route Table Associations for Private Subnets
  6. Internet Gateway (IGW)
  7. Elastic IP (EIP) for NAT Gateway
  8. NAT Gateway
  9. Route Table for Private Subnets
  10. Route Table for Public Subnets
  11. EC2 Instances

Virtual Private Cloud (VPC): A Virtual Private Cloud with the specified CIDR block, DNS hostnames, and support enabled for the VPC.

# main.tf
# VPC
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true

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

Private Subnets: Two public subnets, each associated with a specific availability zone. Each public subnet is configured to allow instances launched with public IP addresses.

# main.tf
# Public subnet
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.vpc.id
count = length(var.public_subnets_cidr)
cidr_block = element(var.public_subnets_cidr, count.index)
availability_zone = element(var.public_AZ, count.index)
map_public_ip_on_launch = true

tags = {
Name = "${var.tag}-${element(var.public_AZ, count.index)}-public-subnet"
}
}

Private Subnets: Two private subnets, with each subnet associated with a specific availability zone, and Instances launched in these private subnets will not have public IP addresses by default.

# main.tf
# Private Subnet
resource "aws_subnet" "private_subnet" {
vpc_id = aws_vpc.vpc.id
count = length(var.private_subnets_cidr)
cidr_block = element(var.private_subnets_cidr, count.index)
availability_zone = element(var.private_AZ, count.index)
map_public_ip_on_launch = false

tags = {
Name = "${var.tag}-${element(var.private_AZ, count.index)}-private-subnet"
}
}

Route Table/Route Table Associations for Public Subnets: Route table associations link each public subnet to a familiar route table for handling their routing.

# main.tf
# Routing tables to route traffic for Public Subnet
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id

tags = {
Name = "${var.tag}-public-route-table"
}
}
# Route table associations for both Public subnet
resource "aws_route_table_association" "public" {
count = length(var.public_subnets_cidr)
subnet_id = element(aws_subnet.public_subnet.*.id, count.index)
route_table_id = aws_route_table.public.id
}

Route Table/Route Table Associations for Private Subnets: Route table associations link each private subnet to a route table for handling their routing.

# main.tf
# Routing tables to route traffic for Private Subnet
resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tag}-private-route-table"
}
}
# Route table associations for both Private subnet
resource "aws_route_table_association" "private" {
count = length(var.private_subnets_cidr)
subnet_id = element(aws_subnet.private_subnet.*.id, count.index)
route_table_id = aws_route_table.private.id
}

Internet Gateway (IGW): An Internet Gateway associated with the VPC, allowing public subnets to communicate with the internet.

# main.tf
#Internet gateway
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
"Name" = "${var.tag}-igw"
}
}

NAT Gateway/Elastic IP (EIP) for NAT Gateway: An Elastic IP is created for use with the NAT Gateway, allowing instances in the private subnets to communicate outbound internet.

# main.tf
# NAT Gateway
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.NAT_eip.id
subnet_id = element(aws_subnet.public_subnet.*.id, 0)
tags = {
Name = "nat-gateway-${var.tag}"
}
}
# Elastic-IP (eip) for NAT
resource "aws_eip" "NAT_eip" {
vpc = true
depends_on = [aws_internet_gateway.igw]
}

aws_instance.public_instance: This block creates EC2 instances in the public subnets.

#main.tf
# EC2 instance in Public Subnet
resource "aws_instance" "public_instance" {
count = length(var.public_subnets_cidr)
ami = var.os
instance_type = var.instance
vpc_security_group_ids = [aws_security_group.public_sg.id]
subnet_id = element(aws_subnet.public_subnet.*.id, count.index)
key_name = "vpn-key"
tags = {
Name = "${var.tag}-public-instance-${count.index + 1}"
}
}
## ssh connectivity
resource "aws_key_pair" "vpn_key" {
key_name = "vpn-key"
public_key = file("~/.ssh/id_rsa.pub")
}
# Creating a security group named sg
resource "aws_security_group" "public_sg" {
# Name, Description and the VPC of the Security Group
name = "sg"
vpc_id = aws_vpc.vpc.id
description = "Security group"
# allowing traffic from our IP on port 22
ingress {
description = "Allow SSH from my computer"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
#instance to being able to talk to the internet
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]

aws_instance.private_instance: This block creates EC2 instances in the private subnets.

# main.tf
# EC2 instance in Private Subnet
resource "aws_instance" "private_instance" {
count = length(var.private_subnets_cidr)
ami = var.os
instance_type = var.instance
subnet_id = element(aws_subnet.private_subnet.*.id, count.index)
key_name = "vpn-key" # Replace with your SSH key pair
tags = {
Name = "${var.tag}-private-instance-${count.index + 1}"
}
}

var.tf

# var.ft
# Replaced with desired values
variable "region" {
default = "us-east-1"
}
variable "tag" {
default = "vpc-setup"
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
description = "CIDR block of the vpc"
}
variable "public_subnets_cidr" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
description = "CIDR block for Public Subnet"
}
variable "private_subnets_cidr" {
type = list(string)
default = ["10.0.3.0/24", "10.0.4.0/24"]
description = "CIDR block for Private Subnet"
}
variable "private_AZ" {
type = list(string)
default = ["us-east-1a"]
}
variable "public_AZ" {
type = list(string)
default = ["us-east-1b"]
}
variable "os" {
default = "ami-08a52ddb321b32a8c"
}
variable "instance" {
default = "t2.micro"
}

TERRAFORM COMMANDS
In the working directory, follow these steps:

RUN
1. terraform init
2. terraform validate
3. terraform plan
4. terraform apply

In summary, the configuration sets up a VPC with public and private subnets, enabling resources to be deployed in a segmented manner. Public subnets can access the internet, while private subnets use a NAT Gateway for outbound traffic. The routing tables ensure that traffic is routed correctly between the subnets and the internet. This architecture is suitable for hosting a variety of applications with different security and access requirements.

Full terraform configuration file can be found Here

Enjoy your learning journey!!