What you will build
This tutorial creates a simple and realistic AWS network layout:
Public side
One public subnet for internet-facing resources such as a load balancer, bastion host, or public web server.
Private side
One private subnet for internal application servers or databases that should not be directly reachable from the internet.
Why use Terraform for AWS VPC?
Terraform lets you define infrastructure as code, which means your VPC design is written in reusable, version-controlled files instead of being created manually in the AWS Console.
That helps with consistency, team collaboration, repeatable environments, faster reviews, and easier disaster recovery.
Real-life example
Imagine a company running three environments: dev, staging, and production. If the network is built manually, each environment may end up slightly different. With Terraform, the team can reuse the same design and only change inputs like CIDR ranges, tags, or Availability Zones.
Full Terraform example: AWS VPC with public and private subnets
This is the full example first. After that, we explain each block in plain English.
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-south-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "cloudnetworking-vpc"
Environment = "dev"
}
}
resource "aws_subnet" "public_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-south-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-1"
Tier = "public"
}
}
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1a"
tags = {
Name = "private-subnet-1"
Tier = "private"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
tags = {
Name = "public-route-table"
}
}
resource "aws_route" "public_internet_route" {
route_table_id = aws_route_table.public_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public_1.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_eip" "nat_eip" {
domain = "vpc"
tags = {
Name = "nat-eip"
}
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat_eip.id
subnet_id = aws_subnet.public_1.id
tags = {
Name = "main-nat-gateway"
}
depends_on = [aws_internet_gateway.igw]
}
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.main.id
tags = {
Name = "private-route-table"
}
}
resource "aws_route" "private_nat_route" {
route_table_id = aws_route_table.private_rt.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
resource "aws_route_table_association" "private_assoc" {
subnet_id = aws_subnet.private_1.id
route_table_id = aws_route_table.private_rt.id
}
Block-by-block explanation
1) Terraform and provider block
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-south-1"
}
This tells Terraform which provider to use and which AWS region to create resources in.
Real-life example: if your company deploys most workloads in
Mumbai, you may standardize on ap-south-1 for dev or regional production systems.
2) VPC block
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "cloudnetworking-vpc"
Environment = "dev"
}
}
This creates your private cloud network. The CIDR block 10.0.0.0/16
gives you a large address range to divide into smaller subnets later.
Real-life example: think of the VPC as the boundary of your company’s cloud office. Everything inside it belongs to your environment.
3) Public subnet block
resource "aws_subnet" "public_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-south-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-1"
Tier = "public"
}
}
A public subnet is used for resources that need direct or indirect internet access.
The setting map_public_ip_on_launch = true allows instances launched here
to receive public IP addresses automatically.
Real-life example: an Application Load Balancer or a bastion host could live in this subnet.
4) Private subnet block
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1a"
tags = {
Name = "private-subnet-1"
Tier = "private"
}
}
This subnet is for internal workloads that should not be directly exposed to the internet.
Real-life example: app servers, internal APIs, queue processors, or databases are commonly placed in private subnets.
5) Internet Gateway block
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
The Internet Gateway is what connects your VPC to the public internet. Without it, public-facing resources cannot send or receive internet traffic.
Real-life example: a public website needs this path so customers can access the frontend.
6) Public route table and route
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
tags = {
Name = "public-route-table"
}
}
resource "aws_route" "public_internet_route" {
route_table_id = aws_route_table.public_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public_1.id
route_table_id = aws_route_table.public_rt.id
}
This sends all non-local traffic to the Internet Gateway. That is what makes the subnet behave like a public subnet.
Real-life example: a web server in this subnet can now answer requests from users on the internet.
7) Elastic IP and NAT Gateway
resource "aws_eip" "nat_eip" {
domain = "vpc"
tags = {
Name = "nat-eip"
}
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat_eip.id
subnet_id = aws_subnet.public_1.id
tags = {
Name = "main-nat-gateway"
}
depends_on = [aws_internet_gateway.igw]
}
The NAT Gateway lives in the public subnet and uses an Elastic IP. Private resources can send outbound traffic through it without becoming directly reachable from the internet.
Real-life example: a backend server in a private subnet may need software updates, package downloads, or calls to third-party APIs.
8) Private route table and route to NAT
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.main.id
tags = {
Name = "private-route-table"
}
}
resource "aws_route" "private_nat_route" {
route_table_id = aws_route_table.private_rt.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
resource "aws_route_table_association" "private_assoc" {
subnet_id = aws_subnet.private_1.id
route_table_id = aws_route_table.private_rt.id
}
This route table gives the private subnet outbound access through the NAT Gateway. It does not make the subnet public.
Real-life example: your app server can pull container images or call payment APIs, but customers still cannot connect directly to it from the internet.
How traffic flows in this VPC
- A user on the internet reaches a public-facing component in the public subnet.
- That component forwards requests to application resources in the private subnet.
- Private workloads answer the request and return the response through the same trusted path.
- If private workloads need outbound internet access, they use the NAT Gateway.
Real-world example: online shopping application
Imagine you are building an online shopping website.
Public subnet
A load balancer accepts traffic from customers who visit your website.
Private subnet
Application servers process orders, user logins, and payments away from the public internet.
In a more advanced design, the database would also live in private subnets, usually across multiple Availability Zones for resilience.
Common beginner mistakes
- Putting databases in public subnets
- Forgetting to associate the correct route table with the subnet
- Assuming a subnet is public just because an instance has a public IP
- Using overlapping CIDR ranges across environments
- Creating only one subnet in one Availability Zone for production
- Forgetting that NAT Gateway adds cost
Frequently asked questions
Should I learn VPC before Terraform?
Yes. Terraform becomes much easier to understand when you already know what a VPC, subnet, route table, Internet Gateway, and NAT Gateway do.
Can a private subnet access the internet?
Yes. A private subnet can access the internet for outbound traffic through a NAT Gateway or NAT instance, but it does not normally allow direct inbound internet access.
Do I need a NAT Gateway in every lab?
No. Many labs skip NAT Gateway to reduce cost. But it is useful to understand because it is common in production network design.
What should I learn after this page?
Next, learn subnets, route tables, Security Groups, NACLs, and then move into multi-AZ VPC design.