LAB03_AWS_Custom VPC with Public and Private Subnets

Custom VPC with Public and Private Subnets

☁ AWS ⭐ Beginner VPC Subnets Internet Gateway Route Tables

Scenario

Your team is moving away from the default AWS VPC, which exposes all subnets to the internet by default. You need to build a proper network foundation with a clear separation between public-facing resources (load balancers) and private resources (application servers, databases).

Provision a custom VPC with one public subnet and one private subnet in a single Availability Zone, with an Internet Gateway for public egress.

Your Objectives
  • Create an aws_vpc with CIDR 10.0.0.0/16 and DNS hostnames enabled.
  • Create a public subnet (10.0.1.0/24) with map_public_ip_on_launch = true.
  • Create a private subnet (10.0.2.0/24) with no public IP assignment.
  • Create an aws_internet_gateway and attach it to the VPC.
  • Create a public aws_route_table with a default route via the IGW and associate it with the public subnet.
  • Output the VPC ID, public subnet ID, and private subnet ID.

Additional Context

The private subnet has no route to the internet in this lab — that is intentional. The key insight here is that the route table association is what makes a subnet "public" or "private" — not anything about the subnet resource itself.

versions.tf

# versions.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

variables.tf

# variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "vpc_cidr"   { type = string; default = "10.0.0.0/16" }
variable "az"         { type = string; default = "us-east-1a" }
variable "environment"{ type = string; default = "dev" }

main.tf

# main.tf

provider "aws" {
  region = var.aws_region
}

# VPC — the isolated network boundary for all your AWS resources.
# enable_dns_hostnames: lets EC2 instances get DNS names like ec2-x-x-x-x.compute-1.amazonaws.com
# enable_dns_support:   required for Route53 resolver and DNS resolution to work inside the VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "main-vpc", Environment = var.environment, ManagedBy = "terraform" }
}

# Public subnet — resources here CAN be assigned public IPs.
# map_public_ip_on_launch = true: any EC2 in this subnet auto-gets a public IP.
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = var.az
  map_public_ip_on_launch = true

  tags = { Name = "public-subnet", Environment = var.environment }
}

# Private subnet — resources here get only private IPs.
# No route to the internet (yet). A NAT Gateway would be needed for outbound access.
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = var.az

  tags = { Name = "private-subnet", Environment = var.environment }
}

# Internet Gateway — the door between your VPC and the public internet.
# Without this, NO traffic can flow in or out of the VPC.
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = { Name = "main-igw", Environment = var.environment }
}

# Public route table — directs traffic destined for the internet (0.0.0.0/0) via the IGW.
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  # Default route: all traffic not matching a more specific route goes to the IGW.
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = { Name = "public-rt", Environment = var.environment }
}

# Associate the public route table with the public subnet.
# This is what makes the subnet "public" — the route to the IGW.
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

outputs.tf

# outputs.tf
output "vpc_id"            { value = aws_vpc.main.id }
output "public_subnet_id"  { value = aws_subnet.public.id }
output "private_subnet_id" { value = aws_subnet.private.id }
output "igw_id"            { value = aws_internet_gateway.main.id }

Workflow Commands

terraform init
terraform plan
terraform apply

# Verify route table is attached to public subnet
aws ec2 describe-route-tables \
  --filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)"

terraform destroy
✓ Expected Output After Apply
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.

Outputs:

igw_id            = "igw-0abc1234def56789"
private_subnet_id = "subnet-0def9876543210abc"
public_subnet_id  = "subnet-0abc1234567890def"
vpc_id            = "vpc-0123456789abcdef0"