By Oliver Awa. Updated Oct 12, 2024. 1st Published on Oct 12, 2022 Learn more about Oliver
We will create a template that deploys a VPC, with a pair of public subnets, private subnets for an application tier and a data tier across two Availability Zones. It deploys an internet gateway, with a default route on the public subnets. It deploys a pair of NAT gateways (one in each AZ), and default routes for them in the private subnets
Please visit our networking page to get a good understanding of VPC Foundational Concepts & Terminologies
A VPC is an isolated virtual network that you define. You have complete control over your virtual networking environment, including selection of your own IP address range, creation of subnets, and configuration of route tables and network gateways.
Here is the architecture diagram of what you will provision and deploys with terraform code
We will not go deep into what terraform is, but if you want to, visit this link Get started with Terraform
You can find the full code of this project at my github account
Note: This project is just help you understand how a simple VPC can be deploy using Terraform. In future projects, we will update this file to incorporate variables, count and modules.
Terraform is an open-source infrastructure as a code (IAC) tool that allows to create, manage & deploy the production-ready environment.
Terraform plugins called providers let Terraform interact with cloud platforms and other services via their application programming interfaces (APIs)
To deploy infrastructure with Terraform:
Hard coding AWS Credentials(ACCESS_KEY, SECRET_KEY) inside terraform file might compromise the security of your infrastructure by exposing AWS Root Account credentials. So it is always recommended to follow good security practices for handling the credentials by seting your Access Keys as the environment variables
To use your IAM credentials to authenticate the Terraform AWS provider, set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as environment variable using the command below.
export AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
export AWS_SECRETE_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
Or if you’re on Windows:
SET AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
SET AWS_SECRETE_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
Or if you’re on PowershellPaste the following text into PowerShell to set the AWS environment variables.:
$Env: AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
$Env: AWS_SECRETE_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx
Run the above command to export AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY variables into the environment variables. After exporting the above variables you do not need to hard code or refer to /.aws/credentials file.
You can simply write you are terraforming configuration and terraform will fetch the AWS Credentials from the environment variables
The set of files used to describe infrastructure in Terraform is known as a Terraform configuration.
Terraform configuration file would ideally have lot of elements known asblocks as provider, resource etcetera.
This is a Syntax of how Terraform Configuration file block is formatted
"" "" {
# Block body
= # Argument
}
There are ample amount of BLOCK_TYPE available in Terraform and the resource is primary and all others are to support building that specified resource.
Some of the Terraform blocks (elements) and their purpose is given below
Use resource blocks to define components of your infrastructure. A resource might be a physical or virtual component such as an EC2 instance, or it can be a logical resource such as a Heroku application.
Resource blocks have two strings before the block: the resource type and the resource name.
Resource blocks contain arguments which you use to configure the resource. Arguments can include things like machine sizes, disk image names, or VPC IDs
Having repeated static values in your Terraform code can create more work for you in the future. That is where variables come in to make your job easier. Variables allow you to have a central source where you can import values from. Instead of updating the value in each location of your code, you only have to update them once in a central source.
Each Terraform configuration must be in its own working directory. Create a directory for your configuration.
In this project, I will use some variables that I will discuss later in this article.
mkdir networking
Change into the directory.
cd networking
Create a main.tf file to define your infrastructure and add each of the following sections or blocks to the main.tf file .
sudo nano main.tf
Use the below code to set our Provider to AWS and set our Region to us-east-1.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# Create VPC
# terraform aws create vpc
resource "aws_vpc" "vpc" {
cidr_block = "${var.vpc_cidr}"
instance_tenancy = "default"
enable_dns_hostnames = "true"
enable_dns_support = "true"
tags = {
Name = "Test VPC"
}
}
This block of code incorporate variables and as explain above ,Variables allow you to have a central source where you can import values from.
The variable to the vpc code block is as seen below
variable "vpc-cidr" {
default = "192.168.0.0/16"
description = "VPC CIDR Block"
type = string
}
Notice that we are setting a description so developers know what the variable is for, a type developers know what type of input is used, and a default of “192.168.0.0/16”.When this resource is referenced when applied, it will look for the variable vpc_cidr which we created.
We will use this approche for the rest of projects
For this project, I will create in the main.tf file a total 6 subnets for the front-end tier and back-end tier with a mixture of public & private subnet.
# Create Public Subnet 1
# terraform aws create subnet
resource "aws_subnet" "public-subnet-1" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.public-subnet-1-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "Public Subnet 1"
}
}
# Create Public Subnet 2
# terraform aws create subnet
resource "aws_subnet" "public-subnet-2" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.public-subnet-2-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
tags = {
Name = "Public Subnet 2"
}
}
# Create Private Subnet 1
# terraform aws create subnet
resource "aws_subnet" "private-subnet-1" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-1-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 1 | App Tier"
}
}
# Create Private Subnet 2
# terraform aws create subnet
resource "aws_subnet" "private-subnet-2" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-2-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 2 | App Tier"
}
}
# Create Private Subnet 3
# terraform aws create subnet
resource "aws_subnet" "private-subnet-3" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-3-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 3 | Database Tier"
}
}
# Create Private Subnet 4
# terraform aws create subnet
resource "aws_subnet" "private-subnet-4" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-4-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 4 | Database Tier"
}
}
We will need an Internet Gateway to allow our public subnets to connect to the Internet.
# Create Internet Gateway and Attach it to VPC
# terraform aws create internet gateway
resource "aws_internet_gateway" "internet-gateway" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "Test IGW"
}
}
Just saying that our subnets are public does not make it so. We will need to create a route table and Associate our Web Layer subnets. The web-rt route table creates a route in our VPC to our Internet Gateway for CIDR 0.0.0.0/0.
# Create Route Table and Add Public Route
# terraform aws create route table
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 1 to "Public Route Table"
# terraform aws associate subnet with route table
resource "aws_route_table_association" "public-subnet-1-route-table-association" {
subnet_id = aws_subnet.public-subnet-1.id
route_table_id = aws_route_table.public-route-table.id
}
# Associate Public Subnet 2 to "Public Route Table"
# terraform aws associate subnet with route table
resource "aws_route_table_association" "public-subnet-2-route-table-association" {
subnet_id = aws_subnet.public-subnet-2.id
route_table_id = aws_route_table.public-route-table.id
}
By default all subnets are associated with the default route table which is set to private by default.
For this project, we have choose to create individual rout table for our subnets in the back-end
resource "aws_route_table" "app-tier-route-table-1" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-1.id}"
}
tags = {
Name = "app-tier-route-table-1"
}
}
resource "aws_route_table" "app-tier-route-table-2" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-2.id}"
}
tags = {
Name = "app-tier-route-table-2"
}
}
resource "aws_route_table" "data-tier-route-table-1" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-1.id}"
}
tags = {
Name = "data-tier-route-table-1"
}
}
resource "aws_route_table" "data-tier-route-table-2" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-2.id}"
}
tags = {
Name = "data-tier-route-table-2"
}
}
#Create Elastic IP
resource "aws_eip" "nat-1" {
tags = {
Name = "EIP"
}
}
#Create Elastic IP
resource "aws_eip" "nat-2" {
tags = {
Name = "EIP"
}
}
For security purpose, Our private subnet can not connect to the internet directly. So in order access the private subnet through the internet , a nat gateway must be provision in the public and an Elastic ip assigned
#I have created this NAT Gateway in public subnet and
#assigned the above created Elastic IP to it
resource "aws_nat_gateway" "nat-gateway-1" {
allocation_id = "${aws_eip.nat-1.id}"
subnet_id = "${aws_subnet.public-subnet-1.id}"
tags = {
Name = "nat-gateway-1"
}
# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.internet-gateway]
}
resource "aws_nat_gateway" "nat-gateway-2" {
allocation_id = "${aws_eip.nat-2.id}"
subnet_id = "${aws_subnet.public-subnet-2.id}"
tags = {
Name = "nat-gateway-2"
}
# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.internet-gateway]
}
For security purpose, we will store our terraform in an already created s3 bucket. so create an s3 bucket in your cloud environment and use the bucket name in the backend resource. in our case we have name our bucket as automate-iac
terraform {
backend "s3" {
bucket = "automate-iac"
key = "remote.tfstate"
region = "us-east-1"
}
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# Create VPC
# terraform aws create vpc
resource "aws_vpc" "vpc" {
cidr_block = "${var.vpc-cidr}"
instance_tenancy = "default"
enable_dns_hostnames = true
tags = {
Name = "Test VPC"
}
}
# Create Internet Gateway and Attach it to VPC
# terraform aws create internet gateway
resource "aws_internet_gateway" "internet-gateway" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "Test IGW"
}
}
# Create Public Subnet 1
# terraform aws create subnet
resource "aws_subnet" "public-subnet-1" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.public-subnet-1-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "Public Subnet 1"
}
}
# Create Public Subnet 2
# terraform aws create subnet
resource "aws_subnet" "public-subnet-2" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.public-subnet-2-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
tags = {
Name = "Public Subnet 2"
}
}
# Create Route Table and Add Public Route
# terraform aws create route table
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 1 to "Public Route Table"
# terraform aws associate subnet with route table
resource "aws_route_table_association" "public-subnet-1-route-table-association" {
subnet_id = aws_subnet.public-subnet-1.id
route_table_id = aws_route_table.public-route-table.id
}
# Associate Public Subnet 2 to "Public Route Table"
# terraform aws associate subnet with route table
resource "aws_route_table_association" "public-subnet-2-route-table-association" {
subnet_id = aws_subnet.public-subnet-2.id
route_table_id = aws_route_table.public-route-table.id
}
# Create Private Subnet 1
# terraform aws create subnet
resource "aws_subnet" "private-subnet-1" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-1-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 1 | App Tier"
}
}
# Create Private Subnet 2
# terraform aws create subnet
resource "aws_subnet" "private-subnet-2" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-2-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 2 | App Tier"
}
}
# Create Private Subnet 3
# terraform aws create subnet
resource "aws_subnet" "private-subnet-3" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-3-cidr}"
availability_zone = "us-east-1a"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 3 | Database Tier"
}
}
# Create Private Subnet 4
# terraform aws create subnet
resource "aws_subnet" "private-subnet-4" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.private-subnet-4-cidr}"
availability_zone = "us-east-1b"
map_public_ip_on_launch = false
tags = {
Name = "Private Subnet 4 | Database Tier"
}
}
#Create Elastic IP
resource "aws_eip" "nat-1" {
tags = {
Name = "EIP"
}
}
#Create Elastic IP
resource "aws_eip" "nat-2" {
tags = {
Name = "EIP"
}
}
#I have created this NAT Gateway in public subnet and
#assigned the above created Elastic IP to it
resource "aws_nat_gateway" "nat-gateway-1" {
allocation_id = "${aws_eip.nat-1.id}"
subnet_id = "${aws_subnet.public-subnet-1.id}"
tags = {
Name = "nat-gateway-1"
}
# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.internet-gateway]
}
resource "aws_nat_gateway" "nat-gateway-2" {
allocation_id = "${aws_eip.nat-2.id}"
subnet_id = "${aws_subnet.public-subnet-2.id}"
tags = {
Name = "nat-gateway-2"
}
# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.internet-gateway]
}
resource "aws_route_table" "app-tier-route-table-1" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-1.id}"
}
tags = {
Name = "app-tier-route-table-1"
}
}
resource "aws_route_table" "app-tier-route-table-2" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-2.id}"
}
tags = {
Name = "app-tier-route-table-2"
}
}
resource "aws_route_table" "data-tier-route-table-1" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-1.id}"
}
tags = {
Name = "data-tier-route-table-1"
}
}
resource "aws_route_table" "data-tier-route-table-2" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_nat_gateway.nat-gateway-2.id}"
}
tags = {
Name = "data-tier-route-table-2"
}
}
# VPC Variables
variable "region" {
default = "us-east-1"
description = "AWS Region"
type = string
}
variable "vpc-cidr" {
default = "192.168.0.0/16"
description = "VPC CIDR Block"
type = string
}
variable "public-subnet-1-cidr" {
default = "192.168.0.0/24"
description = "Public Subnet 1 CIDR Block"
type = string
}
variable "public-subnet-2-cidr" {
default = "192.168.1.0/24"
description = "Public Subnet 2 CIDR Block"
type = string
}
variable "private-subnet-1-cidr" {
default = "192.168.2.0/24"
description = "Private Subnet 1 CIDR Block"
type = string
}
variable "private-subnet-2-cidr" {
default = "192.168.3.0/24"
description = "Private Subnet 2 CIDR Block"
type = string
}
variable "private-subnet-3-cidr" {
default = "192.168.4.0/24"
description = "Private Subnet 3 CIDR Block"
type = string
}
variable "private-subnet-4-cidr" {
default = "192.168.5.0/24"
description = "Private Subnet 4 CIDR Block"
type = string
}
terraform {
backend "s3" {
bucket = "automate-iac"
key = "remote.tfstate"
region = "us-east-1"
}
}
output "vpcid" {
value = aws_vpc.vpc.id
}
# Subnets
output "public_subnet_ids" {
description = "List of IDs of all public subnets in the VPC"
value = aws_subnet.public-subnet-1.*.id
}
output "private_subnet_ids" {
description = "List of IDs of all private subnets in the VPC"
value = aws_subnet.private-subnet-1.*.id
}
To provision the VPC, have the main.tf, variable.tf and output.tf in one directory and run the following command
erraform downloads the aws provider and installs it in a hidden subdirectory of your current working directory, named .terraform. The terraform init command prints out which version of the provider was installed. Terraform also creates a lock file named .terraform.lock.hcl which specifies the exact provider versions used, so that you can control when you want to update the providers used for your project.>
When you applied your configuration, Terraform wrote data into a file called terraform.tfstate. Terraform stores the IDs and properties of the resources it manages in this file, so that it can update or destroy those resources going forward.
The Terraform state file is the only way Terraform can track which resources it manages, and often contains sensitive information, so you must store your state file securely and restrict access to only trusted team members who need to manage your infrastructure. In production, we recommend storing your state remotely
You can Inspect the current state using terraform show command
Once the resource creation finishes, you can look into your Aws account and check all the resources you specified
To delete our infrastructure run terraform destroy . When prompted type Yes. This command will delete all the infrastructure that we created.
Congratulations on creating a Three-Tier AWS Architecture!
In our next project which will be part 2 of this project, we will make use of some terraform block elements local, modules, data a parameter like Count to simplify and reduce the size of our project. We will also provision more resources and create an s3 bucket to store the statefile