Cloud 35 min read

AWS OpenTofu #109: VPC Fundamentals

Leave the default VPC behind. Architect and provision a secure, multi-AZ custom Virtual Private Cloud from scratch with CIDR partitioning, public/private subnets, Internet Gateways, and route tables.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide β€’ Part 9 of 15

AWS + OpenTofu Level 100: Infrastructure as Code from Zero

Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.

In every lab up to this point, our EC2 instances and security groups were provisioned inside the AWS Default VPC.

While the Default VPC is convenient for quick demos and initial learning, deploying production workloads in it is a major security and architectural failure.

Why?

  1. The Flat Network Hazard: In the Default VPC, every single subnet is a public subnet. Every subnet has an automatic route to the Internet Gateway (0.0.0.0/0 -> igw).
  2. Accidental Database Exposure: If a developer launches a PostgreSQL or MongoDB database in the Default VPC and toggles public IP assignment on, that database is exposed to the public internet. If credentials leak or an unpatched vulnerability exists, the database can be breached directly from anywhere in the world.
  3. No Network Segmentation: You cannot isolate internal microservices, private backend APIs, or data pipelines from public-facing load balancers.
  4. Hardcoded Overlapping CIDRs: Every Default VPC in AWS uses the exact same CIDR block (172.31.0.0/16). If your organization later needs to connect two VPCs via VPC Peering, Transit Gateway, or an on-premises VPN, overlapping CIDRs make routing mathematically impossible.

Welcome to Module 04: Networking & Troubleshooting!

In this lab, you will design and provision your very own custom Virtual Private Cloud (VPC) from scratch using OpenTofu. You will master IPv4 CIDR calculations, partition your network into Public and Private subnets across multiple Availability Zones for High Availability, attach an Internet Gateway (IGW), build custom Route Tables, and verify the complete network topology using the AWS CLI.


🎯 What You Will Learn

AWS Networking ArchitectureOpenTofu Network DeclarationsEnterprise Isolation Standards
β€’ Virtual Private Cloud (VPC) primitivesβ€’ Declaring aws_vpc with parameterized CIDRsβ€’ Public vs. Private subnet isolation rules
β€’ IPv4 CIDR blocks (/16 vs /24) & mathβ€’ Multi-AZ subnet distribution via data sourcesβ€’ Multi-AZ redundancy across distinct AZs
β€’ The 5 AWS Reserved IPs per subnetβ€’ Declaring aws_internet_gatewayβ€’ Strict tag conventions (Tier = "public")
β€’ Route Tables, default routes, & associationsβ€’ Building custom aws_route_table & associationsβ€’ Auditing routes & subnets via aws ec2

πŸ’° Estimated Lab Cost

ResourceConfigurationFree Tier CostStandard Account (Past Free Tier)
Amazon Virtual Private Cloud (VPC)1 Custom VPC (/16)$0.00$0.00 (Always 100% Free)
AWS Subnets (Public & Private)4 Subnets across 2 AZs$0.00$0.00 (Always 100% Free)
AWS Internet Gateway (IGW)1 Attached Gateway$0.00$0.00 (Always 100% Free)
AWS Route Tables & Associations2 Custom Route Tables$0.00$0.00 (Always 100% Free)
OpenTofu CLILocal binary execution$0.00$0.00 (Free & Open Source)
Total Estimated CostComplete Hands-On Session$0.00$0.00 (100% Free)

[!NOTE] Networking Primitives are Free: AWS charges $0.00 for VPCs, Subnets, Route Tables, and Internet Gateways. An Internet Gateway has no hourly fee, no setup fee, and no recurring charge. Whether on Free Tier or a standard commercial account, this lab is 100% free.


🌐 Architecture: Production Multi-AZ Custom VPC

Here is the production-grade network topology we will construct with OpenTofu:

+---------------------------------------------------------------------------------------------------+
| AWS CUSTOM VPC: 10.0.0.0/16 (e.g. ap-southeast-1)                                                 |
|                                                                                                   |
|   +-------------------------------------------------------------------------------------------+   |
|   | INTERNET GATEWAY (IGW): igw-xxxx (Attached to VPC)                                        |   |
|   +-------------------------------------------------------------------------------------------+   |
|                                     β”‚                                                             |
|                                     β–Ό Route: 0.0.0.0/0 -> IGW                                     |
|   +-------------------------------------------------------------------------------------------+   |
|   | PUBLIC ROUTE TABLE                                                                        |   |
|   +-------------------------------------------------------------------------------------------+   |
|                 β”‚                                                           β”‚                     |
|                 β–Ό Subnet Association                                        β–Ό Subnet Association  |
|   +───────────────────────────────────────────+   +───────────────────────────────────────────+   |
|   | PUBLIC SUBNET 1 (AZ-a)                    |   | PUBLIC SUBNET 2 (AZ-b)                    |   |
|   | CIDR: 10.0.1.0/24 (251 usable IPs)        |   | CIDR: 10.0.2.0/24 (251 usable IPs)        |   |
|   | Auto-assign Public IP: YES                |   | Auto-assign Public IP: YES                |   |
|   | (Hosts ALBs, Web Servers, Bastions)       |   | (Hosts ALBs, Web Servers, Bastions)       |   |
|   +───────────────────────────────────────────+   +───────────────────────────────────────────+   |
|                                                                                                   |
| ====================================== AIR GAP BOUNDARY ========================================= |
|                                                                                                   |
|   +-------------------------------------------------------------------------------------------+   |
|   | PRIVATE ROUTE TABLE (NO ROUTE TO IGW! Local VPC Traffic Only)                             |   |
|   +-------------------------------------------------------------------------------------------+   |
|                 β”‚                                                           β”‚                     |
|                 β–Ό Subnet Association                                        β–Ό Subnet Association  |
|   +───────────────────────────────────────────+   +───────────────────────────────────────────+   |
|   | PRIVATE SUBNET 1 (AZ-a)                   |   | PRIVATE SUBNET 2 (AZ-b)                   |   |
|   | CIDR: 10.0.11.0/24 (251 usable IPs)       |   | CIDR: 10.0.12.0/24 (251 usable IPs)       |   |
|   | Auto-assign Public IP: NO                 |   | Auto-assign Public IP: NO                 |   |
|   | (Hosts RDS Databases, Internal APIs)      |   | (Hosts RDS Databases, Internal APIs)      |   |
|   +───────────────────────────────────────────+   +───────────────────────────────────────────+   |
+---------------------------------------------------------------------------------------------------+

πŸ”’ Part 1: IPv4 CIDR Math Made Simple

To design cloud networks, you must understand CIDR (Classless Inter-Domain Routing) notation.

An IPv4 address consists of 32 bits (four 8-bit octets). The prefix slash (/16, /24) indicates how many bits are fixed for the network prefix, leaving the remaining bits for host IP addresses.

Formula: Total IPs = 2^(32 - Prefix)
CIDR BlockCalculationTotal IP AddressesCommon Cloud Usage
10.0.0.0/16$2^{(32 - 16)} = 2^{16}$65,536Standard VPC Supernet
10.0.1.0/24$2^{(32 - 24)} = 2^8$256Standard Subnet Tier
10.0.1.0/28$2^{(32 - 28)} = 2^4$16Tiny Subnet / Micro-endpoints

⚠️ The 5 AWS Reserved IPs in Every Subnet

In traditional networking, only 2 IPs are reserved (Network and Broadcast). In AWS, 5 IP addresses are permanently reserved in every subnet and cannot be assigned to your instances:

For subnet 10.0.1.0/24:

  1. 10.0.1.0: Network Address (Always reserved by RFC 1122).
  2. 10.0.1.1: VPC Router (The default gateway router for the subnet).
  3. 10.0.1.2: AWS DNS Server (AmazonProvidedDNS mapping: base + 2).
  4. 10.0.1.3: Future AWS Use (Reserved for internal routing features).
  5. 10.0.1.255: Network Broadcast Address (AWS does not support broadcast, but reserves this address).

[!IMPORTANT] Because 5 IPs are reserved, a /24 subnet (256 total IPs) provides exactly 251 usable host IPs.


❓ Part 2: What ACTUALLY Makes a Subnet β€œPublic”?

Many engineers mistakenly believe that setting map_public_ip_on_launch = true makes a subnet public.

That is false. An instance with a public IP in a subnet without an Internet Gateway route is completely unreachable from the outside world!

In AWS networking, a subnet is defined as Public if and only if:

  1. It is associated with a Route Table that contains a default route to an Internet Gateway:
    destination_cidr_block = "0.0.0.0/0"
    gateway_id             = aws_internet_gateway.gw.id

A subnet is Private if its route table only contains the default local VPC route (10.0.0.0/16 -> local), preventing any direct internet traffic from entering or leaving.


πŸ“ Step 1: Set Up Lab Directory Structure

Navigate to your ~/opentofu-labs repository and create the directory for Lab 109:

cd ~/opentofu-labs
mkdir -p lab-109-custom-vpc
cd lab-109-custom-vpc

We will structure our project using dedicated, clean configuration files:

lab-109-custom-vpc/
β”œβ”€β”€ providers.tf             # Engine & AWS Provider setup
β”œβ”€β”€ variables.tf             # Parameterized CIDRs & environment
β”œβ”€β”€ data.tf                  # Dynamic AZ discovery
β”œβ”€β”€ vpc.tf                   # VPC & Internet Gateway
β”œβ”€β”€ subnets.tf               # Public & Private Subnets across 2 AZs
β”œβ”€β”€ routing.tf               # Route Tables & Subnet Associations
└── outputs.tf               # Exported network IDs

βš™οΈ Step 2: Configure Providers & Variables

Create providers.tf:

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = "OpenTofu-Level-100"
      Environment = var.environment
      ManagedBy   = "OpenTofu"
      LabPart     = "109"
    }
  }
}

Create variables.tf:

variable "aws_region" {
  type        = string
  description = "Target AWS Region."
  default     = "ap-southeast-1"
}

variable "environment" {
  type        = string
  description = "Target deployment environment."
  default     = "dev"
}

variable "vpc_cidr" {
  type        = string
  description = "The main IPv4 CIDR block for the custom VPC."
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  type        = list(string)
  description = "CIDR blocks for public subnets in AZ-1 and AZ-2."
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  type        = list(string)
  description = "CIDR blocks for private subnets in AZ-1 and AZ-2."
  default     = ["10.0.11.0/24", "10.0.12.0/24"]
}

Create data.tf to discover healthy Availability Zones dynamically (as mastered in Lab 105):

data "aws_availability_zones" "available" {
  state = "available"

  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

πŸ—οΈ Step 3: Declare VPC & Internet Gateway (vpc.tf)

Create vpc.tf:

# 1. Custom Virtual Private Cloud (VPC)
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_support   = true # Required for AWS private DNS resolution
  enable_dns_hostnames = true # Required for public DNS hostnames on EC2

  tags = {
    Name = "${var.environment}-custom-vpc"
  }
}

# 2. Internet Gateway (IGW) Attached to the VPC
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.environment}-igw"
  }
}

Why DNS Settings Matter:

  • enable_dns_support = true: Enables the AmazonProvidedDNS resolver at 10.0.0.2.
  • enable_dns_hostnames = true: Ensures instances with public IPs receive a human-readable public DNS hostname (e.g. ec2-xx-xx-xx-xx.compute-1.amazonaws.com).

🌐 Step 4: Declare Public & Private Subnets (subnets.tf)

Create subnets.tf.

Notice how we use data.aws_availability_zones.available.names[0] and names[1] to dynamically distribute subnets across two distinct physical Availability Zones:

# ==============================================================================
# PUBLIC SUBNETS (Multi-AZ: AZ-1 & AZ-2)
# ==============================================================================

# Public Subnet 1 (AZ-1)
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[0] # 10.0.1.0/24
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true # Auto-assigns public IPv4 to instances

  tags = {
    Name = "${var.environment}-public-subnet-1"
    Tier = "public"
    AZ   = data.aws_availability_zones.available.names[0]
  }
}

# Public Subnet 2 (AZ-2)
resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[1] # 10.0.2.0/24
  availability_zone       = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.environment}-public-subnet-2"
    Tier = "public"
    AZ   = data.aws_availability_zones.available.names[1]
  }
}

# ==============================================================================
# PRIVATE SUBNETS (Multi-AZ: AZ-1 & AZ-2)
# ==============================================================================

# Private Subnet 1 (AZ-1)
resource "aws_subnet" "private_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.private_subnet_cidrs[0] # 10.0.11.0/24
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = false # STRICT: Never assign public IPs to private tier!

  tags = {
    Name = "${var.environment}-private-subnet-1"
    Tier = "private"
    AZ   = data.aws_availability_zones.available.names[0]
  }
}

# Private Subnet 2 (AZ-2)
resource "aws_subnet" "private_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.private_subnet_cidrs[1] # 10.0.12.0/24
  availability_zone       = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.environment}-private-subnet-2"
    Tier = "private"
    AZ   = data.aws_availability_zones.available.names[1]
  }
}

🚦 Step 5: Declare Route Tables & Associations (routing.tf)

Now we connect the subnets to their respective routing layers in routing.tf:

# ==============================================================================
# 1. PUBLIC ROUTING (Default Route points to Internet Gateway)
# ==============================================================================

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  # The Default Route (0.0.0.0/0) -> Sends all non-VPC internet traffic to the IGW
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "${var.environment}-public-rt"
    Tier = "public"
  }
}

# Associate Public Subnet 1 with Public Route Table
resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

# Associate Public Subnet 2 with Public Route Table
resource "aws_route_table_association" "public_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}

# ==============================================================================
# 2. PRIVATE ROUTING (Isolated: Local VPC traffic only, NO route to IGW)
# ==============================================================================

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  # NOTE: No 0.0.0.0/0 route! Only the implicit local route (10.0.0.0/16 -> local) exists.

  tags = {
    Name = "${var.environment}-private-rt"
    Tier = "private"
  }
}

# Associate Private Subnet 1 with Private Route Table
resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private.id
}

# Associate Private Subnet 2 with Private Route Table
resource "aws_route_table_association" "private_2" {
  subnet_id      = aws_subnet.private_2.id
  route_table_id = aws_route_table.private.id
}

πŸ“€ Step 6: Expose Network Outputs (outputs.tf)

Create outputs.tf:

output "vpc_id" {
  description = "The unique ID of the custom VPC."
  value       = aws_vpc.main.id
}

output "vpc_cidr" {
  description = "The main CIDR block allocated to the VPC."
  value       = aws_vpc.main.cidr_block
}

output "internet_gateway_id" {
  description = "The ID of the attached Internet Gateway."
  value       = aws_internet_gateway.gw.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs (for ALBs and Web tier)."
  value       = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}

output "private_subnet_ids" {
  description = "List of private subnet IDs (for Databases and App tier)."
  value       = [aws_subnet.private_1.id, aws_subnet.private_2.id]
}

output "public_route_table_id" {
  description = "The ID of the public route table."
  value       = aws_route_table.public.id
}

output "private_route_table_id" {
  description = "The ID of the private route table."
  value       = aws_route_table.private.id
}

πŸš€ Step 7: Preview & Provision (tofu apply)

Initialize the directory:

tofu init
tofu fmt
tofu validate

Preview the plan:

tofu plan

Notice the plan summary:

Plan: 10 to add, 0 to change, 0 to destroy.
  + aws_vpc.main
  + aws_internet_gateway.gw
  + aws_subnet.public_1
  + aws_subnet.public_2
  + aws_subnet.private_1
  + aws_subnet.private_2
  + aws_route_table.public
  + aws_route_table.private
  + aws_route_table_association.public_1
  + aws_route_table_association.public_2
  + aws_route_table_association.private_1
  + aws_route_table_association.private_2

Apply the configuration:

tofu apply -auto-approve
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0123456789abcdef0]
aws_internet_gateway.gw: Creating...
aws_route_table.private: Creating...
aws_subnet.private_2: Creating...
aws_subnet.public_1: Creating...
aws_subnet.public_2: Creating...
aws_subnet.private_1: Creating...
aws_internet_gateway.gw: Creation complete after 1s [id=igw-0123456789abcdef0]
aws_route_table.public: Creating...
aws_route_table.private: Creation complete after 1s [id=rtb-0private12345678]
aws_subnet.private_1: Creation complete after 1s [id=subnet-0priv1111111111]
aws_subnet.public_1: Creation complete after 1s [id=subnet-0pub1111111111]
aws_subnet.private_2: Creation complete after 1s [id=subnet-0priv2222222222]
aws_subnet.public_2: Creation complete after 1s [id=subnet-0pub2222222222]
aws_route_table.public: Creation complete after 1s [id=rtb-0public12345678]
aws_route_table_association.private_1: Creating...
aws_route_table_association.private_2: Creating...
aws_route_table_association.public_1: Creating...
aws_route_table_association.public_2: Creating...
aws_route_table_association.public_1: Creation complete after 1s
aws_route_table_association.public_2: Creation complete after 1s
aws_route_table_association.private_1: Creation complete after 1s
aws_route_table_association.private_2: Creation complete after 1s

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

In approximately 6 seconds, OpenTofu orchestrated ten interrelated API calls to build an enterprise multi-AZ network foundation.


πŸ§ͺ Step 8: Live Network Audit with AWS CLI

Let’s verify the live network topology in AWS using the CLI:

1. Audit VPC & DNS Attributes

VPC_ID=$(tofu output -raw vpc_id)

aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --query "Vpcs[0].{VpcId:VpcId, Cidr:CidrBlock, State:State, DhcpOptions:DhcpOptionsId}" \
  --output table

Output:

--------------------------------------------------------------
|                        DescribeVpcs                        |
+----------------+---------------------+----------+----------+
|      Cidr      |     DhcpOptions     |  State   |  VpcId   |
+----------------+---------------------+----------+----------+
|  10.0.0.0/16   |  dopt-0123456789... |  available| vpc-xxxx |
+----------------+---------------------+----------+----------+

2. Audit Subnets & Usable IPv4 Counts

Query all subnets in your custom VPC and observe the available IP addresses:

aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=$VPC_ID" \
  --query "Subnets[].{SubnetId:SubnetId, Cidr:CidrBlock, AZ:AvailabilityZone, AvailableIps:AvailableIpAddressCount, PublicIpOnLaunch:MapPublicIpOnLaunch}" \
  --output table

Output:

-----------------------------------------------------------------------------------------
|                                    DescribeSubnets                                    |
+------------------+---------------+--------------+--------------------+----------------+
|        AZ        | AvailableIps  |    Cidr      | PublicIpOnLaunch   |   SubnetId     |
+------------------+---------------+--------------+--------------------+----------------+
| ap-southeast-1a  | 251           | 10.0.1.0/24  | True               | subnet-pub1    |
| ap-southeast-1b  | 251           | 10.0.2.0/24  | True               | subnet-pub2    |
| ap-southeast-1a  | 251           | 10.0.11.0/24 | False              | subnet-priv1   |
| ap-southeast-1b  | 251           | 10.0.12.0/24 | False              | subnet-priv2   |
+------------------+---------------+--------------+--------------------+----------------+

Notice:

  • AvailableIps: 251 confirms the 5 AWS Reserved IPs calculation ($256 - 5 = 251$).
  • PublicIpOnLaunch is True on the public subnets and False on the private subnets.

3. Audit Route Tables & Target Gateways

Confirm that only the public route table routes to the Internet Gateway:

aws ec2 describe-route-tables \
  --filters "Name=vpc-id,Values=$VPC_ID" \
  --query "RouteTables[].{RouteTableId:RouteTableId, Routes:Routes[].{Destination:DestinationCidrBlock, Target:GatewayId}}" \
  --output json

Output:

[
  {
    "RouteTableId": "rtb-public",
    "Routes": [
      { "Destination": "10.0.0.0/16", "Target": "local" },
      { "Destination": "0.0.0.0/0", "Target": "igw-xxxx" }
    ]
  },
  {
    "RouteTableId": "rtb-private",
    "Routes": [
      { "Destination": "10.0.0.0/16", "Target": "local" }
    ]
  }
]

The audit is conclusive:

  • The public route table routes 0.0.0.0/0 out to the Internet Gateway.
  • The private route table only routes 10.0.0.0/16 internally (Target: local).

🧹 Step 9: Clean Teardown (tofu destroy)

Tear down the network infrastructure:

tofu destroy -auto-approve

Output:

aws_route_table_association.public_1: Destroying...
aws_route_table_association.public_2: Destroying...
aws_route_table_association.private_1: Destroying...
aws_route_table_association.private_2: Destroying...
...
aws_subnet.public_1: Destroying...
aws_route_table.public: Destroying...
aws_internet_gateway.gw: Destroying...
aws_vpc.main: Destroying...
Destroy complete! Resources: 10 destroyed.

Why OpenTofu’s Dependency Graph Was Critical Here:

If you try to delete an Internet Gateway in the AWS Console while subnets are still associated or while instances are running, AWS throws a DependencyViolation. OpenTofu computes the reverse graph automatically: it deletes associations first, then subnets and route tables, then detaches the IGW, and finally deletes the VPC container cleanly.


πŸ› οΈ Common Troubleshooting & Gotchas

1. Error: Overlapping CIDR block

  • Cause: Trying to assign subnets whose IP ranges overlap (e.g. 10.0.1.0/24 and 10.0.1.0/25).
  • Fix: Check CIDR boundaries. For /24 subnets, the third octet must increment: 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24.

2. Error: DependencyViolation: The vpc 'vpc-xxx' has dependencies and cannot be deleted

  • Cause: An EC2 instance, network interface (ENI), or managed service (like an RDS database) was manually created inside the VPC outside of OpenTofu.
  • Fix: OpenTofu cannot delete a VPC if non-managed resources are still running inside it. Terminate any orphaned instances or ENIs first.

3. Forgetting Subnet Route Table Associations

  • Gotcha: If you declare a public subnet and create a public route table, but forget to declare aws_route_table_association, AWS automatically associates the subnet with the VPC Main (Default) Route Table, turning your intended public subnet into an unreachable orphan. Always declare explicit associations.

πŸ‹οΈ Hands-On Challenge

Before proceeding to Lab 110:

  1. Add a Dedicated Database Subnet Tier: In variables.tf, add a variable:
    variable "database_subnet_cidrs" {
      type    = list(string)
      default = ["10.0.21.0/24", "10.0.22.0/24"]
    }
  2. In subnets.tf, declare aws_subnet.database_1 and aws_subnet.database_2 across AZ-1 and AZ-2 with map_public_ip_on_launch = false and tag Tier = "database".
  3. In routing.tf, associate both database subnets with aws_route_table.private.id.
  4. Run tofu plan and observe how OpenTofu expands your network into a 3-tier enterprise architecture (Public Web -> Private App -> Private Database).

πŸ“– Summary & What You Learned

In this foundational networking lab, you built the isolated backbone for enterprise cloud architectures:

  • Default VPC Dangers: Understood why flat default networks expose databases and create overlapping IP conflicts.
  • CIDR Math: Calculated /16 (65,536 IPs) and /24 (256 IPs) boundaries, and accounted for the 5 AWS Reserved IPs per subnet.
  • Public vs. Private Definition: Proved that a subnet is only β€œpublic” if its route table explicitly routes 0.0.0.0/0 to an attached Internet Gateway.
  • Multi-AZ Redundancy: Distributed public and private subnets across two distinct Availability Zones for high availability.
  • Automated Network Teardown: Experienced how OpenTofu cleanly navigates complex cloud networking dependency graphs.

πŸš€ What’s Next? (Lab 110)

We now have an isolated, multi-AZ custom VPC. But right now, it is an empty highway with no cars driving on it.

In [AWS OpenTofu #110: EC2 Inside Your Own VPC], we will:

  • Combine everything we’ve built: launch an EC2 instance directly inside our custom public subnet.
  • Connect via SSM Session Manager from inside our custom network.
  • Trace the complete end-to-end packet flow from local terminal to custom VPC.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.