Creating custom VPC on AWS using OpenTofu
Tags: opentofu, infrastructureascode, aws, terraform
Published: 2024-05-13 02:36:58

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ovt31e8aowdlfg2gvc0t.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9xt1hgq3jlhdt8fd879.png)

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsrbj9smne9gqb2vs4fb.png)


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

![AWS VPC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1t79ezw4fvzbox4thv6.png)


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](https://www.linkedin.com/in/vinod827/) and [Twitter](https://twitter.com/vinod827).