Creating custom VPC on AWS using OpenTofu
Vinod Kumar
Posted on May 13, 2024
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
Posted on May 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.