DEV Community

Cover image for Creating custom VPC on AWS using OpenTofu
Vinod Kumar
Vinod Kumar

Posted on

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.

OpenTofu

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:-

AWS Virtual Private Cloud (Custom) in Northern Virginia Region

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

Image description

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


Enter fullscreen mode Exit fullscreen mode

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"
}


Enter fullscreen mode Exit fullscreen mode

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"
    }
}


Enter fullscreen mode Exit fullscreen mode

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"
    }
}


Enter fullscreen mode Exit fullscreen mode

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"
  }
}


Enter fullscreen mode Exit fullscreen mode

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]
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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



tofu validate
tofu plan


Enter fullscreen mode Exit fullscreen mode

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.


Enter fullscreen mode Exit fullscreen mode

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



tofu apply



Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Voila! its all done :-)

All your custom VPC resources will be created.

Output

AWS VPC

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



tofu destroy

Enter fullscreen mode Exit fullscreen mode




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.

Top comments (0)