In Java the network is somebody else's problem: new Socket(host, 443) either
connects or throws, and whatever sits between your JVM and the internet was set up by an ops team you never
met. On AWS you draw that network yourself — a private address range (the VPC), subnets pinned
to one data centre each, an internet gateway as the only door, and route tables that decide
whether a packet may leave at all. None of that costs anything. The one box that lets a machine in a
private subnet reach the internet — the NAT gateway — is billed by the hour whether or not a
byte crosses it, and it is the line that surprises everyone on their first AWS bill. So this page builds the
network every later cloud item runs in, as a reusable Terraform module, and leaves the NAT gateway out on
purpose.
A route table is a lookup, not a rule chain: for each packet, the route with the
longest matching prefix wins. 10.0.0.0/16 → local keeps traffic inside the VPC;
0.0.0.0/0 → igw matches everything else and sends it out. A subnet whose route table has no
0.0.0.0/0 line is private — not because of a flag, but because there is no way out.
One VPC, 10.0.0.0/16, split across two Availability Zones of ap-south-1 (Mumbai).
Each AZ gets a public subnet (10.0.0.0/24, 10.0.1.0/24 — its route table
sends 0.0.0.0/0 to the internet gateway, and anything launched in it gets a public IP) and a
private subnet (10.0.10.0/24, 10.0.11.0/24 — the VPC's main route table,
which knows only local). Boxes carry the AWS name on the first line and the Terraform resource
on the second; dashed boxes do not exist in the step you picked. The heading in the scene is the line
terraform plan would print for that step, and the readout adds up an approximate monthly
figure from the resources that are on — prices are AWS's published us-east-1 rates (Amazon VPC pricing page,
read 2026-09-10: $0.045 per NAT-gateway-hour, $0.045 per GB it processes, $0.005 per public IPv4 address per
hour) over a 30-day month of 720 hours; ap-south-1's NAT rate is higher, so verify on the pricing page today.
The NAT gateway wears the red ring the whole time: greyed until step 3 turns it on.
terraform apply before that e-mail.$B there),
terraform version prints 1.11 or later, and aws sts get-caller-identity with
AWS_PROFILE=lbv-admin names an assumed role, not :root.aws ec2 describe-availability-zones --region ap-south-1 --query 'AvailabilityZones[].ZoneName'
lists ap-south-1a and ap-south-1b (and ap-south-1c).~/lbv-cloud/modules/vpc/ for the module and ~/lbv-cloud/04-vpc/
for the root module that calls it. Later cloud items call the same module or read this root's outputs.1. The module's inputs — modules/vpc/variables.tf. The subnets are a map, keyed
by a name you choose. The key becomes part of each subnet's address in the state
(aws_subnet.this["public-a"]), which is the whole point of the next step:
# modules/vpc/variables.tf
variable "name" {
description = "Prefix for every Name tag"
type = string
}
variable "cidr_block" {
description = "The VPC's private address range"
type = string
}
variable "subnets" {
description = "One entry per subnet: key => AZ, CIDR, and whether it is public"
type = map(object({
az = string
cidr = string
public = bool
}))
}
2. The resources — modules/vpc/main.tf. One VPC, one subnet per map entry via
for_each, one internet gateway, one public route table with one route to that gateway, and one
association per public subnet (a for expression filters the map). The private subnets are
deliberately left unassociated, so AWS gives them the VPC's main route table — local only.
There is no aws_nat_gateway and no aws_eip anywhere in the file:
# modules/vpc/main.tf resource "aws_vpc" "this" { cidr_block = var.cidr_block enable_dns_support = true enable_dns_hostnames = true tags = { Name = var.name } } # for_each over a map: each subnet is addressed by its key (aws_subnet.this["public-a"]), # so removing one entry destroys that subnet only -- a count list would renumber the rest. resource "aws_subnet" "this" { for_each = var.subnets vpc_id = aws_vpc.this.id availability_zone = each.value.az cidr_block = each.value.cidr map_public_ip_on_launch = each.value.public tags = { Name = "${var.name}-${each.key}" } } resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id tags = { Name = var.name } } resource "aws_route_table" "public" { vpc_id = aws_vpc.this.id tags = { Name = "${var.name}-public" } } resource "aws_route" "public_internet" { route_table_id = aws_route_table.public.id destination_cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.this.id } resource "aws_route_table_association" "public" { for_each = { for k, s in var.subnets : k => s if s.public } subnet_id = aws_subnet.this[each.key].id route_table_id = aws_route_table.public.id } # No aws_nat_gateway, on purpose. The private subnets keep the VPC's main route table, # which knows only 10.0.0.0/16 -> local: nothing in them reaches the internet, and nothing bills.
3. The module's outputs — modules/vpc/outputs.tf. Three values every later item needs:
the VPC id, and the two lists of subnet ids, split by the same public flag:
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = [for k, s in aws_subnet.this : s.id if var.subnets[k].public]
}
output "private_subnet_ids" {
value = [for k, s in aws_subnet.this : s.id if !var.subnets[k].public]
}
4. The root module's pins and backend — 04-vpc/versions.tf. The same bucket as cloud-03,
a new key: each root module gets its own state object, so a mistake in this one can never rewrite
cloud-03's. Change only the bucket name. default_tags stamps every resource, which is how you
find them in Cost Explorer later:
# 04-vpc/versions.tf
terraform {
required_version = "~> 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
backend "s3" {
bucket = "lbv-tfstate-yourname-x7q2"
key = "lbv-track/04-vpc/terraform.tfstate"
region = "ap-south-1"
use_lockfile = true
encrypt = true
}
}
provider "aws" {
region = "ap-south-1"
default_tags {
tags = {
Project = "lbv-track"
Item = "cloud-04"
}
}
}
5. Call the module — 04-vpc/main.tf, with the four subnets as data:
# 04-vpc/main.tf
module "vpc" {
source = "../modules/vpc"
name = "lbv"
cidr_block = "10.0.0.0/16"
subnets = {
public-a = { az = "ap-south-1a", cidr = "10.0.0.0/24", public = true }
public-b = { az = "ap-south-1b", cidr = "10.0.1.0/24", public = true }
private-a = { az = "ap-south-1a", cidr = "10.0.10.0/24", public = false }
private-b = { az = "ap-south-1b", cidr = "10.0.11.0/24", public = false }
}
}
6. Re-export its outputs — 04-vpc/outputs.tf (a module's outputs are only visible to the
root that called it; these make them visible to terraform output and to later items):
# 04-vpc/outputs.tf
output "vpc_id" {
value = module.vpc.vpc_id
}
output "public_subnet_ids" {
value = module.vpc.public_subnet_ids
}
output "private_subnet_ids" {
value = module.vpc.private_subnet_ids
}
7. Init, check, plan. Read the plan before applying it — the count and the absence of one word are the two things to check:
cd ~/lbv-cloud/04-vpc export AWS_PROFILE=lbv-admin terraform init # Initializing the backend... Successfully configured the backend "s3"! # Initializing modules... - vpc in ../modules/vpc terraform fmt -recursive -check .. terraform validate # Success! The configuration is valid. terraform plan -out=tfplan # Plan: 10 to add, 0 to change, 0 to destroy. # 1 aws_vpc + 4 aws_subnet + 1 aws_internet_gateway + 1 aws_route_table # + 1 aws_route + 2 aws_route_table_association = 10 terraform show -no-color tfplan | grep -c aws_nat_gateway # 0
8. Apply — the first resources of the track that exist on AWS:
terraform apply tfplan
# Apply complete! Resources: 10 added, 0 changed, 0 destroyed.
# Outputs:
# private_subnet_ids = [ "subnet-…", "subnet-…" ]
# public_subnet_ids = [ "subnet-…", "subnet-…" ]
# vpc_id = "vpc-…"
VPC=$(terraform output -raw vpc_id); echo "$VPC"
aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC --query 'Subnets[].[AvailabilityZone,CidrBlock,MapPublicIpOnLaunch]' --output table
prints four rows — 10.0.0.0/24 and 10.0.1.0/24 with True,
10.0.10.0/24 and 10.0.11.0/24 with False, two in
ap-south-1a and two in ap-south-1b. Without --query the same call
shows "MapPublicIpOnLaunch": true exactly twice.aws ec2 describe-route-tables --filters Name=vpc-id,Values=$VPC --query 'RouteTables[].Routes[].[DestinationCidrBlock,GatewayId]' --output text
prints 0.0.0.0/0 with an igw-… id once, and 10.0.0.0/16 local twice
(the main table and the public one).aws ec2 describe-nat-gateways --filter Name=vpc-id,Values=$VPC --query 'NatGateways[].NatGatewayId'
prints [], and aws ec2 describe-addresses --query 'Addresses[].PublicIp' prints
[] — no NAT gateway, no Elastic IP.aws s3 ls s3://$B/lbv-track/04-vpc/ (in a new shell,
set B= to your cloud-03 bucket name first) lists
terraform.tfstate, next to (not instead of) cloud-03's lbv-track/terraform.tfstate.lbv → Resource map — four
subnets in two AZs, two route tables, one internet gateway, and no network connection box for a NAT.None now — keep the VPC. The Fargate items build into it: cloud-06 reads vpc_id and
public_subnet_ids from this state, and cloud-07 runs the service in the public subnets (cloud-05's
ECR repository is regional and lives outside any VPC, so it does not need this one). A VPC with
no NAT gateway, no Elastic IP and nothing running in it costs nothing to keep — VPCs, subnets, route tables
and internet gateways carry no charge of their own.
When the whole track is over, and only after cloud-06/07 have destroyed everything that runs inside it:
cd ~/lbv-cloud/04-vpc VPC=$(terraform output -raw vpc_id) # before the destroy: afterwards there is no output to read aws ec2 describe-network-interfaces --filters Name=vpc-id,Values=$VPC --query 'NetworkInterfaces[].NetworkInterfaceId' # [] -- nothing still runs in it (a leftover ENI is what makes a VPC destroy hang) terraform destroy # Destroy complete! Resources: 10 destroyed. aws ec2 describe-vpcs --vpc-ids $VPC # An error occurred (InvalidVpcID.NotFound) ... -- the VPC is gone aws ec2 describe-addresses --query 'Addresses[].PublicIp' # [] -- no Elastic IPs left
What remains and what it costs, approx., verify on the pricing page today: after this page, the VPC and its 10 resources — ~$0.00/mo. The public IPv4 charge ($0.005 per address per hour) starts only when something with a public IP runs in a public subnet; that is cloud-06's line, and it is counted there.
There is no teardown box on this page, because the VPC stays. Press the button once the five Verify lines above are true.
This is self-attestation — the site cannot see your AWS account, so pressing the button is you telling The Path the plan said 10 to add, four subnets exist, and no NAT gateway does.
0.0.0.0/0 → igw line makes it public, the absence of one makes
it private. And for_each over a map, not count over a list, is how a module keeps
each subnet's address stable when the list changes.