Terraform AWS VPC Tutorial - Public, Private, and Isolated Subnets¶
- Mentorship/On-the-Job Support/Consulting - Calendar or me@antonputra.com
- Source Code: GitHub Repo.
Create an AWS VPC using Terraform¶
In the previous section, we covered the basics of Terraform, how it works, and how to authenticate with AWS using different approaches, including IAM roles.
- Let's go ahead and create a
0-terraform
folder that we'll initially use to create VPC components.
- Next we need to declare AWS Provider, in the previos session we created IAM role for the terraform, you can keep using that but to keep it simple and self contained I'll use default authentication mechanism for this lesson.
0-providers.tf
provider "aws" {
region = "us-east-2"
}
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
- Next step is to create AWS VPC itself and enable DNS since it's commonly used by many compoentns including EKS and EFS and you will be requred to enable it anyway.
1-vpc.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "dev-main"
}
}
- It's very common that you would need to download packages or interact with internet in some way, for that we need internet gateway. Even if you only will use private subnets you need this component.
2-igw.tf
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "dev-igw"
}
}
Create Public AWS Subnets¶
- To make applications highly available, you need to create subnets in at least two different availability zones, which are separate data centers.
3-public-subnets.tf
resource "aws_subnet" "public_zone1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.0.0/19"
availability_zone = "us-east-2a"
map_public_ip_on_launch = true
tags = {
"Name" = "dev-public-us-east-2a"
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
resource "aws_subnet" "public_zone2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.32.0/19"
availability_zone = "us-east-2b"
map_public_ip_on_launch = true
tags = {
"Name" = "dev-public-us-east-2b"
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
- Subnets by themselves only have local routes that allow applications to interact within the VPC. To allow applications to reach the Internet, you need to create a public route table and associate it with the subnets.
4-public-routes.tf
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "dev-public"
}
}
resource "aws_route_table_association" "public_zone1" {
subnet_id = aws_subnet.public_zone1.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_zone2" {
subnet_id = aws_subnet.public_zone2.id
route_table_id = aws_route_table.public.id
}
Create Private AWS Subnets¶
- Next let's create private subnets which would not get public Ip addresses and will not be routable from the internet but you still be able to use NAT gateway to reach internet, like to download necesary packes or access remote REST API.
5-nat.tf
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "dev-nat"
}
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_zone1.id
tags = {
Name = "dev-nat"
}
depends_on = [aws_internet_gateway.igw]
}
- Next, create the subnets.
6-private-subnets.tf
resource "aws_subnet" "private_zone1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.64.0/19"
availability_zone = "us-east-2a"
tags = {
"Name" = "dev-private-us-east-2a"
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
resource "aws_subnet" "private_zone2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.96.0/19"
availability_zone = "us-east-2b"
tags = {
"Name" = "dev-private-us-east-2b"
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
- Next, create a private route table with a default route to a NAT gateway and associate the private subnets with this route table.
7-private-routes.tf
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
tags = {
Name = "dev-private"
}
}
resource "aws_route_table_association" "private_zone1" {
subnet_id = aws_subnet.private_zone1.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "private_zone2" {
subnet_id = aws_subnet.private_zone2.id
route_table_id = aws_route_table.private.id
}
Create Isolated AWS Subnets¶
- In some cases, you may want to completely isolate applications, such as databases, from the Internet. To do this, create isolated subnets and use only the local route provided by the VPC.
8-isolated-subnets.tf
resource "aws_subnet" "isolated_zone1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.128.0/19"
availability_zone = "us-east-2a"
tags = {
"Name" = "dev-isolated-us-east-2a"
}
}
resource "aws_subnet" "isolated_zone2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.160.0/19"
availability_zone = "us-east-2b"
tags = {
"Name" = "dev-isolated-us-east-2b"
}
}
Before we continue, let us destroy the infrastructure.
Optimize Terraform Code¶
-
Let us copy and paste specific Terraform code and refactor it for portability.
-
First, create the
1-terraform
folder in the project directory.
- Next, create a
0-locals.tf
file in the1-terraform
folder to define locals. First, specify the AWS region for the infrastructure.
- Next, insert the AWS provider configuration into the
1-provider.tf
file in the1-terraform
folder.
1-providers.tf
provider "aws" {
region = local.region
}
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
- Let us add the
vpc_cidr
local variable to the0-locals.tf
file.
- Copy the
1-vpc.tf
file into the1-terraform
folder and replace thecidr_block
value with thelocal.vpc_cidr
variable.
2-vpc.tf
resource "aws_vpc" "main" {
cidr_block = local.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${local.env}-main"
}
}
- Next, recreate the VPC using the Terraform configuration in the
1-terraform
folder. Run terraform apply to provision the resources.
- Add local variables for the public subnets.
0-locals.tf
- Copy the locals {
...
azs = ["us-east-2a", "us-east-2b"]
public_subnets = ["10.0.0.0/19", "10.0.32.0/19"]
}
2-igw.tf
file into the 1-terraform
folder and replace the Name tag with the local.env
variable.
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.env}-igw"
}
}
- Copy the
3-public-subnets.tf
file into the1-terraform
folder and refactor it to use a count
4-public-subnets.tf
resource "aws_subnet" "public" {
count = length(local.public_subnets)
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnets[count.index]
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
"Name" = "${local.env}-public-${local.azs[count.index]}"
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
- Copy the
4-public-routes.tf
file into the1-terraform
folder.
5-public-routes.tf
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "${local.env}-public"
}
}
resource "aws_route_table_association" "public" {
count = length(local.public_subnets)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
- Let us verify that public subnets can be dynamically created using the count variable in the
3-public-subnets.tf
file.
- Copy the
5-nat.tf
file into the1-terraform
folder.
6-nat.tf
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${local.env}-nat"
}
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${local.env}-nat"
}
depends_on = [aws_internet_gateway.igw]
}
- Add local variables for private subnets to the
0-locals.tf
file in the1-terraform
folder.
0-locals.tf
locals {
...
private_subnets = {
public_1 = {
cidr = "10.0.64.0/19"
az = "us-east-2a"
}
public_2 = {
cidr = "10.0.96.0/19"
az = "us-east-2b"
}
}
}
- Copy the
6-private-subnets.tf
file into the1-terraform
folder.
7-private-subnets.tf
resource "aws_subnet" "private" {
for_each = local.private_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
"Name" = "${local.env}-private-${each.value.az}"
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/dev-demo" = "owned"
}
}
- Copy the
7-private-routes.tf
file into the1-terraform
.
8-private-routes.tf
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
tags = {
Name = "${local.env}-private"
}
}
resource "aws_route_table_association" "private" {
for_each = local.private_subnets
subnet_id = aws_subnet.private[each.key].id
route_table_id = aws_route_table.private.id
}
- Verify that private subnets can be created using the configuration in the
6-private-subnets.tf
file.
- Use the built-in
cidrsubnet
function to divide CIDR ranges for private subnets in the6-private-subnets.tf
.
0-locals.tf
locals {
...
private_subnets = {
public_1 = {
cidr = cidrsubnet(local.vpc_cidr, 3, 2)
az = "us-east-2a"
}
public_2 = {
cidr = cidrsubnet(local.vpc_cidr, 3, 3)
az = "us-east-2b"
}
}
}
- Verify that no changes are detected by running terraform plan in the
1-terraform
folder.
- Create a conditional variable in variables.tf to enable on-demand creation of isolated subnets in the
1-terraform
folder.
- Copy the
8-isolated-subnets.tf
file into the1-terraform
folder.
9-isolated-subnets.tf
resource "aws_subnet" "isolated_zone1" {
count = local.create_isolated_subnets ? 1 : 0
vpc_id = aws_vpc.main.id
cidr_block = "10.0.128.0/19"
availability_zone = "us-east-2a"
tags = {
"Name" = "dev-isolated-us-east-2a"
}
}
resource "aws_subnet" "isolated_zone2" {
count = local.create_isolated_subnets ? 1 : 0
vpc_id = aws_vpc.main.id
cidr_block = "10.0.160.0/19"
availability_zone = "us-east-2b"
tags = {
"Name" = "dev-isolated-us-east-2b"
}
}
- Verify that isolated subnets can be created conditionally by setting the conditional variable to
true
andfalse
.
- Create a security group configuration in a new
10-sg.tf
file.
10-sg.tf
resource "aws_security_group" "web" {
name = "web"
vpc_id = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["63.10.10.10/32"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
- Create a security group in AWS.
- Add a local variable for ingress rules to the
0-locals.tf
file.
- Update the
10-sg.tf
file to dynamically generate ingress rules using the local variable.
10-sg.tf
resource "aws_security_group" "web" {
name = "web"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = local.ingress_rules
content {
from_port = ingress.key
to_port = ingress.key
protocol = "tcp"
cidr_blocks = [ingress.value]
}
}
}
- Verify that no changes are required by running
terraform plan
in the1-terraform
folder.
- Clean Up.