DEV Community

Cover image for How to Build AWS VPC Using Terraform - Step By Step
Chinthaka Dinadasa 🇱🇰 for AWS Community Builders

Posted on • Originally published at javatodev.com

How to Build AWS VPC Using Terraform - Step By Step

As more organizations migrate their infrastructure to the cloud, the need for resilient and secure Virtual Private Cloud (VPC) networks becomes more important. AWS offers a powerful VPC service that allows users to create their own virtual network in the cloud.

However, setting up and managing a VPC can be difficult and time-consuming. That's where Terraform comes in as an open-source infrastructure as a code tool that can help automate creating and managing AWS resources.

In this article, we'll explore how to use Terraform to create a VPC on AWS, including the various components that make up a VPC and the steps required to implement them. By the end of this article, you'll know exactly how to use Terraform to build your own secure and scalable VPC network on AWS.

The architecture we are going to setup

This setup includes the following components within the VPC that we will set up using Terraform.

  • AWC VPC – Base Virtual Private Cloud that creates in the AWS.
  • Subnets – Includes private and public subnets within 2 availability zones. (Here I’ve used 2 availability zones, but feel free to add more AZs per your requirement.)
  • Internet Gateway – will allow instances under the public subnet to access the public internet and users to access things on the public subnet via the internet.
  • NAT Gateway – will allow instances under private subnet to access public internet when there is any requirement like download patches, os updates, etc. But a NAT gateway allows only one-way communication that will not open a private subnet to the public internet.
  • Security Groups – Security groups will be used in allowing who can access what inside this architecture
  • Route tables – used to build the routing for IGW and NAT gateway.

AWS VPC Architecture Diagram with Subnets, NAT Gateway, and Internet Gateway

Prerequisites

To follow this tutorial you will need:

  • The Terraform CLI (1.2.0+) installed.
  • The AWS CLI is installed.
  • AWS account and associated credentials that allow you to create resources.

Let’s focus on building the VPC now.

Initiate Terraform Configuration

First, we need to initiate the terraform folder where we can keep all the configurations related to our terraform setup.

$ mkdir terraform-configuration
$ cd terraform-configuration
Enter fullscreen mode Exit fullscreen mode

create main.tf and copy the following content where we define providers to the terraform.

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

  required_version = ">= 1.2.0"
}
Enter fullscreen mode Exit fullscreen mode

Then run the following command to initiate terraform in this directory.

$ terraform init
Enter fullscreen mode Exit fullscreen mode

Terraform Init Command response after initiation.

Basic Terraform Commands

We have the ability to validate the config before applying it on a real AWS environment using Terraform Validate.

$ terraform validate
Enter fullscreen mode Exit fullscreen mode

Terraform Validate

Additionally, we can plan the configuration before applying it on a real AWS environment using Terraform plan, Which shows what we are going to publish and the changes that going to apply on the AWS.

$ terraform plan
Enter fullscreen mode Exit fullscreen mode

Sample response on terraform plan

Also we can format the terraform configuration document using following command,

$ terraform fmt
Enter fullscreen mode Exit fullscreen mode

Define input variables for Terraform

Terraform configurations can include variables to make our configuration more dynamic and flexible rather than hard code those whenever needed.

let’s create the vars.tf where we can manage all the variables to the configuration that we are going to build.

variable "aws_region" {
  default = "us-east-1"
}

variable "environment" {
  default = "javatodev"
}

variable "vpc_cidr" {
  default     = "10.0.0.0/16"
  description = "CIDR block of the vpc"
}

variable "public_subnets_cidr" {
  type        = list(any)
  default     = ["10.0.0.0/20", "10.0.128.0/20"]
  description = "CIDR block for Public Subnet"
}

variable "private_subnets_cidr" {
  type        = list(any)
  default     = ["10.0.16.0/20", "10.0.144.0/20"]
  description = "CIDR block for Private Subnet"
}
Enter fullscreen mode Exit fullscreen mode

Define provider with an AWS region

Here we have to use the AWS provider with setting AWS region where we should publish this configuration and build the architecture.

Additionally, here we are referring to variables we have defined in the earlier stage as well.

provider "aws" {
  region = var.aws_region
}
Enter fullscreen mode Exit fullscreen mode

Local Values

A local value assigns a name to an expression, so you can use the name multiple times within a module instead of repeating the expression.

Local values are like a function’s temporary local variables in programming languages.

Now we need to set the following availability zones in this setup.

  • us-east-1a
  • us-east-1b

we set availability zones in this setup as a local value as follows, and refer to those when needed within other instructions.

locals {
  availability_zones = ["${var.aws_region}a", "${var.aws_region}b"]
}
Enter fullscreen mode Exit fullscreen mode

Define VPC and other components

VPC – Virtual Private Network

Now we can start defining the VPC and related components inside the necessary architecture.

Let’s begin with instructions on VPC creation.

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

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a resource that we can create on AWS with Terraform, Let’s apply this VPC and check with the AWS VPC dashboard on that specific region.

$ terraform apply
Enter fullscreen mode Exit fullscreen mode

Sample response on VPC creation with Terraform on AWS

VPC Dashboard Showing newly created VPC with Terraform

Private and Public Subnets Inside VPC

Let’s configure the private and public subnets that we are going to place inside this VPC.

# 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(local.availability_zones, count.index)
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.environment}-${element(local.availability_zones, count.index)}-public-subnet"
    Environment = "${var.environment}"
  }
}

# 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(local.availability_zones, count.index)
  map_public_ip_on_launch = false

  tags = {
    Name        = "${var.environment}-${element(local.availability_zones, count.index)}-private-subnet"
    Environment = "${var.environment}"
  }
}
Enter fullscreen mode Exit fullscreen mode

Internet Gateway and NAT Gateway

Internet gateway allows connecting the VPC to the Internet and to other AWS services through public internet.

NAT gateway that instances in a private subnet can connect to services outside your VPC but external services cannot initiate a connection with those instances.

#Internet gateway
resource "aws_internet_gateway" "ig" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    "Name"        = "${var.environment}-igw"
    "Environment" = var.environment
  }
}

# Elastic-IP (eip) for NAT
resource "aws_eip" "nat_eip" {
  vpc        = true
  depends_on = [aws_internet_gateway.ig]
}

# 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.environment}"
    Environment = "${var.environment}"
  }
}
Enter fullscreen mode Exit fullscreen mode

As per the standard, we should have a NAT gateway on each AZ to access the internet without any issue in a situation like an unhealthy availability zone.

But here we are only creating a single NAT gateway with single elastic IP for the demo purposes.

VPC Resource map with internet gateway and NAT gateway

Route tables – private and public subnets

# Routing tables to route traffic for Private Subnet
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name        = "${var.environment}-private-route-table"
    Environment = "${var.environment}"
  }
}

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

  tags = {
    Name        = "${var.environment}-public-route-table"
    Environment = "${var.environment}"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure public route to use internet gateway for internet access

# Route for Internet Gateway
resource "aws_route" "public_internet_gateway" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.ig.id
}
Enter fullscreen mode Exit fullscreen mode

Configure private route to use a NAT gateway for internet access

# Route for NAT Gateway
resource "aws_route" "private_internet_gateway" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_nat_gateway.nat.id
}
Enter fullscreen mode Exit fullscreen mode

Associate route tables for subnets

Now we have subnets, route tables, and route table access to the internet, Now we can associate these route tables into the correct subnets.

route table for public subnets.

# 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
}
Enter fullscreen mode Exit fullscreen mode

route table for the 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
}
Enter fullscreen mode Exit fullscreen mode

Now we have a correct mapping from VPC, subnet, and route table with NAT gateway and internet gateway.

Subnet association with route tables inside the AWS VPC

So finally terraform configuration should be like the below,

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

  required_version = ">= 1.2.0"
}

provider "aws" {
  region = var.aws_region
}

locals {
  availability_zones = ["${var.aws_region}a", "${var.aws_region}b"]
}

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

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

# 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(local.availability_zones, count.index)
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.environment}-${element(local.availability_zones, count.index)}-public-subnet"
    Environment = "${var.environment}"
  }
}

# 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(local.availability_zones, count.index)
  map_public_ip_on_launch = false

  tags = {
    Name        = "${var.environment}-${element(local.availability_zones, count.index)}-private-subnet"
    Environment = "${var.environment}"
  }
}

#Internet gateway
resource "aws_internet_gateway" "ig" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    "Name"        = "${var.environment}-igw"
    "Environment" = var.environment
  }
}

# Elastic-IP (eip) for NAT
resource "aws_eip" "nat_eip" {
  vpc        = true
  depends_on = [aws_internet_gateway.ig]
}

# 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.environment}"
    Environment = "${var.environment}"
  }
}

# Routing tables to route traffic for Private Subnet
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name        = "${var.environment}-private-route-table"
    Environment = "${var.environment}"
  }
}

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

  tags = {
    Name        = "${var.environment}-public-route-table"
    Environment = "${var.environment}"
  }
}

# Route for Internet Gateway
resource "aws_route" "public_internet_gateway" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.ig.id
}

# Route for NAT Gateway
resource "aws_route" "private_internet_gateway" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_nat_gateway.nat.id
}

# Route table associations for both Public & Private Subnets
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
}

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
}
Enter fullscreen mode Exit fullscreen mode

All done now we have the full VPC setup with all the necessary components to use whenever we need actual application deployments.

Please remember to remove the setup if you are no longer using it. since the NAT gateway and elastic IP are getting billed when in the ACTIVE stage.

$ terraform destroy
Enter fullscreen mode Exit fullscreen mode

Conclusions

In this article, we have discussed How to Build AWS VPC Using Terraform – Step By Step Guide.

The implementation of all these examples and code snippets can be found in our Github repository.

Happy coding.

Top comments (2)

Collapse
 
avinashdalvi_ profile image
Avinash Dalvi

Detailed blog. Thanks for writing this.

Collapse
 
corporatecamel profile image
Corporate Camel • Edited

As per the standard, we should have a NAT gateway on each AZ to access the internet without any issue in a situation like an unhealthy availability zone.
But here we are only creating a single NAT gateway with single elastic IP for the demo purposes.

Great article - very helpful. Just some questions:

  • How can the provided code sample be modified to use a NAT gateway on each AZ?

  • In other words, how can we create multiple NAT gateways with multiple elastic IPs for real world / non-demo scenarios?

Looking forward to your response.