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-terraformfolder 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-terraformfolder in the project directory.
- Next, create a
0-locals.tffile in the1-terraformfolder to define locals. First, specify the AWS region for the infrastructure.
- Next, insert the AWS provider configuration into the
1-provider.tffile in the1-terraformfolder.
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_cidrlocal variable to the0-locals.tffile.
- Copy the
1-vpc.tffile into the1-terraformfolder and replace thecidr_blockvalue with thelocal.vpc_cidrvariable.
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-terraformfolder. 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.tffile into the1-terraformfolder 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.tffile into the1-terraformfolder.
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.tffile.
- Copy the
5-nat.tffile into the1-terraformfolder.
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.tffile in the1-terraformfolder.
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.tffile into the1-terraformfolder.
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.tffile 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.tffile.
- Use the built-in
cidrsubnetfunction 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-terraformfolder.
- Create a conditional variable in variables.tf to enable on-demand creation of isolated subnets in the
1-terraformfolder.
- Copy the
8-isolated-subnets.tffile into the1-terraformfolder.
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
trueandfalse.
- Create a security group configuration in a new
10-sg.tffile.
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.tffile.
- Update the
10-sg.tffile 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 planin the1-terraformfolder.
- Clean Up.