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.
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?
- 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). - 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.
- No Network Segmentation: You cannot isolate internal microservices, private backend APIs, or data pipelines from public-facing load balancers.
- 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 Architecture | OpenTofu Network Declarations | Enterprise 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
| Resource | Configuration | Free Tier Cost | Standard 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 & Associations | 2 Custom Route Tables | $0.00 | $0.00 (Always 100% Free) |
| OpenTofu CLI | Local binary execution | $0.00 | $0.00 (Free & Open Source) |
| Total Estimated Cost | Complete 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 Block | Calculation | Total IP Addresses | Common Cloud Usage |
|---|---|---|---|
10.0.0.0/16 | $2^{(32 - 16)} = 2^{16}$ | 65,536 | Standard VPC Supernet |
10.0.1.0/24 | $2^{(32 - 24)} = 2^8$ | 256 | Standard Subnet Tier |
10.0.1.0/28 | $2^{(32 - 28)} = 2^4$ | 16 | Tiny 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:
10.0.1.0: Network Address (Always reserved by RFC 1122).10.0.1.1: VPC Router (The default gateway router for the subnet).10.0.1.2: AWS DNS Server (AmazonProvidedDNS mapping: base + 2).10.0.1.3: Future AWS Use (Reserved for internal routing features).10.0.1.255: Network Broadcast Address (AWS does not support broadcast, but reserves this address).
[!IMPORTANT] Because 5 IPs are reserved, a
/24subnet (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:
- 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 at10.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: 251confirms the 5 AWS Reserved IPs calculation ($256 - 5 = 251$).PublicIpOnLaunchisTrueon the public subnets andFalseon 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/0out to the Internet Gateway. - The private route table only routes
10.0.0.0/16internally (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/24and10.0.1.0/25). - Fix: Check CIDR boundaries. For
/24subnets, 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:
- 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"] } - In
subnets.tf, declareaws_subnet.database_1andaws_subnet.database_2across AZ-1 and AZ-2 withmap_public_ip_on_launch = falseand tagTier = "database". - In
routing.tf, associate both database subnets withaws_route_table.private.id. - Run
tofu planand 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/0to 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.