Creating custom VPC on AWS using OpenTofu

Creating custom VPC on AWS using OpenTofu

The OpenTofu is a Linux Foundation project which is a complete opensource Infrastructure as Code tool, an alternative to the popular Terraform. This essentially means it supports natively Terraform’s HCL (HashiCorp Configuration Language) to write the infrastructure as code.

In this blog, we will see how we can you OpenTofu as Infrastructure as Code (IaC) to provision a custom Virtual Private Cloud (VPC) on Amazon Web Services.

Following is the internal architecture of our custom VPC that we are going to provision on AWS using the OpenTofu:-

However, let us first compare OpenTofu with other popular IaC tools like AWS Cloud Formation or Terraform.

Now let us go ahead and create the VPC using OpenTofu.

Step 1. Install OpenTofu in your system first.

You can execute the following command on MacOS terminal to install the binary for OpenTofu. For other operating systems, refer this documentation link.

brew update
brew install opentofu

Step 2. Setup the AWS provider configuration

Create a file, say 00_provider.tf and copy the following code. Here we have used a variable for AWS region with default as us-east-1 (Northern Virginia) and AWS as the required provider. Also, to connect to our account, we have mapped this to the profile name myaws (which is there in the local path $HOME/.aws/profile).

variable aws_region {
default = “us-east-1”
description = “AWS region where the resources will be provisioned”
}

# Configure the AWS Provider
terraform {
required_providers {
aws = {
source = “hashicorp/aws”
version = “~> 5.0”
}
# helm = {
# source = “hashicorp/aws”
# version = “~> 2.6”
# }
}
}

# Configure region and profile
provider “aws” {
region = var.aws_region
profile = “myaws”
}

Step 3. Create a custom VPC configuration and save it in a file, say 01_vpc.tf

resource “aws_vpc” “mycustomvpc” {
cidr_block = “10.0.0.0/16”
enable_dns_support = true
enable_dns_hostnames = true

tags = {
“owner” = “vinod”
“Name” = “my custom VPC”
}
}

Step 4. Create Internet Gateway and attach it to the VPC

resource “aws_internet_gateway” “igw” {
vpc_id = aws_vpc.mycustomvpc.id
tags = {
“owner” = “vinod”
“Name” = “IGW”
}
}

Step 5. Create Subnets for the VPC

resource “aws_subnet” “private-us-east-1a” {
vpc_id = aws_vpc.mycustomvpc.id
cidr_block = “10.0.1.0/24”
availability_zone = “us-east-1a”

tags = {
“subnet” = “private-us-east-1a”
“Name” = “Private Subnet”
}
}

resource “aws_subnet” “private-us-east-1b” {
vpc_id = aws_vpc.mycustomvpc.id
cidr_block = “10.0.2.0/24”
availability_zone = “us-east-1b”

tags = {
“subnet” = “private-us-east-1b”
“Name” = “Private Subnet”
}
}

resource “aws_subnet” “public-us-east-1a” {
vpc_id = aws_vpc.mycustomvpc.id
cidr_block = “10.0.3.0/24”
availability_zone = “us-east-1a”
map_public_ip_on_launch = true

tags = {
“subnet” = “public-us-east-1a”
“Name” = “Public Subnet”
}
}

resource “aws_subnet” “public-us-east-1b” {
vpc_id = aws_vpc.mycustomvpc.id
cidr_block = “10.0.4.0/24”
availability_zone = “us-east-1b”
map_public_ip_on_launch = true

tags = {
“subnet” = “public-us-east-1b”
“Name” = “Public Subnet”
}
}

I have created 4 subnets (2 private and 2 public).

Step 6. Create a NAT Gateway and EIP configuration

Create a NAT gateway and attach it to the public subnet. The NAT Gateway allows our instances running within private subnets to access Public Internet for Operating Systems and other software patch updates.

resource “aws_eip” “nat” {
vpc = true

tags = {
“Name” = “EIP”
“Owner” = “Vinod”
}

}

resource “aws_nat_gateway” “nat” {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public-us-east-1a.id

tags = {
“Name” = “NAT Gateway”
“Owner” = “Vinod”
}

# To ensure proper ordering, it is recommended to add an explicit dependency
# on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.igw]
}

Step 7. Create route configuration and its association with subnets

Create two route tables (one as private and another as public) with route to NAT Gateway and Internet Gateway respectively. Associate them to their respective private and public subnets.

resource “aws_route_table” “privateroute” {
vpc_id = aws_vpc.mycustomvpc.id

route {
cidr_block = “0.0.0.0/0”
nat_gateway_id = aws_nat_gateway.nat.id
}

tags = {
Name = “private”
}
}

resource “aws_route_table” “publicroute” {
vpc_id = aws_vpc.mycustomvpc.id

route {
cidr_block = “0.0.0.0/0”
gateway_id = aws_internet_gateway.igw.id
}

tags = {
Name = “public”
}
}

resource “aws_route_table_association” “privateassociation_a” {
subnet_id = aws_subnet.private-us-east-1a.id
route_table_id = aws_route_table.privateroute.id
}
resource “aws_route_table_association” “privateassociation_b” {
subnet_id = aws_subnet.private-us-east-1b.id
route_table_id = aws_route_table.privateroute.id
}
resource “aws_route_table_association” “publicassociation_a” {
subnet_id = aws_subnet.public-us-east-1a.id
route_table_id = aws_route_table.publicroute.id
}
resource “aws_route_table_association” “publicassociation_b” {
subnet_id = aws_subnet.public-us-east-1b.id
route_table_id = aws_route_table.publicroute.id
}

Initialize the tofu project to install all dependencies, modules, etc. by executing on the same directory where all the above .tf files are present

tofu init

To validate our configuration and doing a dry run (without actually provisioning any resources), execute

tofu validate
tofu plan

This will output a summary plan of our change like below for us to review:-

OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create

OpenTofu will perform the following actions:

# aws_eip.nat will be created
+ resource “aws_eip” “nat” {
+ allocation_id = (known after apply)
+ arn = (known after apply)
+ association_id = (known after apply)
+ carrier_ip = (known after apply)
+ customer_owned_ip = (known after apply)
+ domain = (known after apply)
+ id = (known after apply)
+ instance = (known after apply)
+ network_border_group = (known after apply)
+ network_interface = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ ptr_record = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ public_ipv4_pool = (known after apply)
+ tags = {
+ “Name” = “EIP”
+ “Owner” = “Vinod”
}
+ tags_all = {
+ “Name” = “EIP”
+ “Owner” = “Vinod”
}
+ vpc = true
}

# aws_internet_gateway.igw will be created
+ resource “aws_internet_gateway” “igw” {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ “Name” = “IGW”
+ “owner” = “vinod”
}
+ tags_all = {
+ “Name” = “IGW”
+ “owner” = “vinod”
}
+ vpc_id = (known after apply)
}

# aws_nat_gateway.nat will be created
+ resource “aws_nat_gateway” “nat” {
+ allocation_id = (known after apply)
+ association_id = (known after apply)
+ connectivity_type = “public”
+ id = (known after apply)
+ network_interface_id = (known after apply)
+ private_ip = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ip_address_count = (known after apply)
+ secondary_private_ip_addresses = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ “Name” = “NAT Gateway”
+ “Owner” = “Vinod”
}
+ tags_all = {
+ “Name” = “NAT Gateway”
+ “Owner” = “Vinod”
}
}

# aws_route_table.privateroute will be created
+ resource “aws_route_table” “privateroute” {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ carrier_gateway_id = “”
+ cidr_block = “0.0.0.0/0”
+ core_network_arn = “”
+ destination_prefix_list_id = “”
+ egress_only_gateway_id = “”
+ gateway_id = “”
+ ipv6_cidr_block = “”
+ local_gateway_id = “”
+ nat_gateway_id = (known after apply)
+ network_interface_id = “”
+ transit_gateway_id = “”
+ vpc_endpoint_id = “”
+ vpc_peering_connection_id = “”
},
]
+ tags = {
+ “Name” = “private”
}
+ tags_all = {
+ “Name” = “private”
}
+ vpc_id = (known after apply)
}

# aws_route_table.publicroute will be created
+ resource “aws_route_table” “publicroute” {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ carrier_gateway_id = “”
+ cidr_block = “0.0.0.0/0”
+ core_network_arn = “”
+ destination_prefix_list_id = “”
+ egress_only_gateway_id = “”
+ gateway_id = (known after apply)
+ ipv6_cidr_block = “”
+ local_gateway_id = “”
+ nat_gateway_id = “”
+ network_interface_id = “”
+ transit_gateway_id = “”
+ vpc_endpoint_id = “”
+ vpc_peering_connection_id = “”
},
]
+ tags = {
+ “Name” = “public”
}
+ tags_all = {
+ “Name” = “public”
}
+ vpc_id = (known after apply)
}

# aws_route_table_association.privateassociation_a will be created
+ resource “aws_route_table_association” “privateassociation_a” {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}

# aws_route_table_association.privateassociation_b will be created
+ resource “aws_route_table_association” “privateassociation_b” {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}

# aws_route_table_association.publicassociation_a will be created
+ resource “aws_route_table_association” “publicassociation_a” {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}

# aws_route_table_association.publicassociation_b will be created
+ resource “aws_route_table_association” “publicassociation_b” {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}

# aws_subnet.private-us-east-1a will be created
+ resource “aws_subnet” “private-us-east-1a” {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = “us-east-1a”
+ availability_zone_id = (known after apply)
+ cidr_block = “10.0.1.0/24”
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ “Name” = “Private Subnet”
+ “subnet” = “private-us-east-1a”
}
+ tags_all = {
+ “Name” = “Private Subnet”
+ “subnet” = “private-us-east-1a”
}
+ vpc_id = (known after apply)
}

# aws_subnet.private-us-east-1b will be created
+ resource “aws_subnet” “private-us-east-1b” {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = “us-east-1b”
+ availability_zone_id = (known after apply)
+ cidr_block = “10.0.2.0/24”
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ “Name” = “Private Subnet”
+ “subnet” = “private-us-east-1b”
}
+ tags_all = {
+ “Name” = “Private Subnet”
+ “subnet” = “private-us-east-1b”
}
+ vpc_id = (known after apply)
}

# aws_subnet.public-us-east-1a will be created
+ resource “aws_subnet” “public-us-east-1a” {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = “us-east-1a”
+ availability_zone_id = (known after apply)
+ cidr_block = “10.0.3.0/24”
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ “Name” = “Public Subnet”
+ “subnet” = “public-us-east-1a”
}
+ tags_all = {
+ “Name” = “Public Subnet”
+ “subnet” = “public-us-east-1a”
}
+ vpc_id = (known after apply)
}

# aws_subnet.public-us-east-1b will be created
+ resource “aws_subnet” “public-us-east-1b” {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = “us-east-1b”
+ availability_zone_id = (known after apply)
+ cidr_block = “10.0.4.0/24”
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ “Name” = “Public Subnet”
+ “subnet” = “public-us-east-1b”
}
+ tags_all = {
+ “Name” = “Public Subnet”
+ “subnet” = “public-us-east-1b”
}
+ vpc_id = (known after apply)
}

# aws_vpc.mycustomvpc will be created
+ resource “aws_vpc” “mycustomvpc” {
+ arn = (known after apply)
+ cidr_block = “10.0.0.0/16”
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_dns_hostnames = true
+ enable_dns_support = true
+ enable_network_address_usage_metrics = (known after apply)
+ id = (known after apply)
+ instance_tenancy = “default”
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_network_border_group = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ “Name” = “my custom VPC”
+ “owner” = “vinod”
}
+ tags_all = {
+ “Name” = “my custom VPC”
+ “owner” = “vinod”
}
}

Plan: 14 to add, 0 to change, 0 to destroy.

Finally, execute the following command to create the custom VPC on AWS

tofu apply

You will need to confirm with yes when prompted on the terminal. If you wish to avoid that prompt then use the flag as —-auto-approve like shown below

tofu apple –auto-approve

Voila! its all done 🙂

All your custom VPC resources will be created.

Output

If you wish to delete all the resources of the custom VPC, then execute:-

tofu destroy

Summary

In this blog, we have seen what OpenTofu is, how it compares as an open source project with other popular IaC tools and how we can install it in our system to create a custom VPC on Amazon Web Services.

Hope you like the article. Please do share your feedback.

Like always, you will find all the source code used in this blog as a reference at this GitHub project. You can star this GitHub repository to get all updates happening on this active project.

https://github.com/vinod827/k8s-nest/tree/main/iac/aws/terraform/creating-custom-vpc

All k8s manifests lives here. Contribute to vinod827/k8s-nest development by creating an account on GitHub.
github.com

Connect me on LinkedIn and Twitter.