Terraform AWS Networking Guide

Build an AWS VPC using Terraform with public and private subnets

AWS VPC is one of the best Terraform projects for learning real cloud networking. It teaches CIDR planning, public and private subnets, route tables, Internet Gateway, NAT Gateway, security groups, and reusable infrastructure design.

This guide shows how to create an AWS VPC with Terraform using a practical production-style layout: VPC, public subnets, private subnets, route tables, Internet Gateway, NAT Gateway, Elastic IP, security groups, variables, outputs, and module-ready structure.

Best for traffic “Terraform AWS VPC example” is a strong practical search topic.
Best for learning VPC teaches the real AWS networking foundation engineers need.
Best for DevOps Terraform VPC code is common in CI/CD, platform, and landing zone work.
Best next step Turn this into a reusable module for dev, staging, and prod.
Terraform AWS VPC architecture

AWS VPC Architecture with Terraform

A common Terraform AWS VPC design uses one VPC spread across multiple Availability Zones. Public subnets host internet-facing resources such as load balancers or bastion hosts. Private subnets host application servers, databases, Kubernetes nodes, and internal workloads. Internet Gateway enables public subnet internet access, while NAT Gateway allows private subnet workloads to reach the internet without being directly exposed.

Terraform AWS VPC reference architecture

VPC, Availability Zones, public/private subnets, Internet Gateway, NAT Gateway, route tables and security boundaries.

VPC Public Subnets Private Subnets NAT Gateway Route Tables
AWS VPC Architecture with Terraform showing Terraform configuration creating an Amazon VPC with public subnets, private app subnets, private data subnets, Internet Gateway, NAT Gateway, route tables, security groups, and VPC endpoints
For production, use at least two Availability Zones. For stronger resilience, many teams use three Availability Zones where supported by the region and budget.
AWS VPC basics

What is AWS VPC?

Amazon Virtual Private Cloud is a logically isolated network inside AWS. It lets you define IP ranges, subnets, routing, gateways, security groups, network ACLs, VPC endpoints, and connectivity patterns. Most AWS workloads such as EC2, RDS, EKS, ECS, Lambda networking, and load balancers depend on VPC design.

VPC CIDR

The primary IP range for the VPC, such as `10.0.0.0/16`. Subnets are carved from this range.

Public subnet

A subnet with a route to the Internet Gateway. Used for public-facing resources such as load balancers.

Private subnet

A subnet without direct inbound internet routing. Used for application, database, and internal workloads.

Why Terraform for AWS VPC

Why create AWS VPC using Terraform?

VPCs include many connected resources. Manually creating them in the AWS console can lead to inconsistent route tables, subnet mistakes, missing tags, and hard-to-repeat environments. Terraform makes the VPC design repeatable, reviewable, reusable, and suitable for CI/CD automation.

Repeatable networking

Create similar VPC designs across dev, staging, and production with different CIDR values.

Git-based review

Every subnet, route, NAT Gateway, and security rule can be reviewed before deployment.

Module-ready design

Once the VPC pattern is tested, it can become a reusable Terraform module.

Faster delivery

New environments can be created faster using Terraform variables and automation pipelines.

Better governance

Tags, naming, routing, security, and subnet layout can follow approved platform standards.

Drift awareness

Terraform plan helps detect when real AWS networking no longer matches your code.

AWS VPC components

AWS VPC components created by Terraform

Component Terraform resource Purpose Common design note
VPC `aws_vpc` Creates the isolated AWS network. Choose CIDR carefully to avoid future overlap.
Public Subnet `aws_subnet` Subnet for internet-facing resources. Route to Internet Gateway.
Private Subnet `aws_subnet` Subnet for internal workloads. Route outbound traffic through NAT Gateway.
Internet Gateway `aws_internet_gateway` Allows public subnet internet connectivity. Attach to the VPC and route public traffic to it.
Elastic IP `aws_eip` Static public IP for NAT Gateway. NAT Gateway requires Elastic IP.
NAT Gateway `aws_nat_gateway` Allows private subnet workloads to access internet outbound. Use one per AZ for high availability in production.
Route Table `aws_route_table` Controls traffic routing for subnets. Use separate public and private route tables.
Security Group `aws_security_group` Stateful firewall for resources. Keep rules minimal and specific.
Terraform project structure

Recommended folder structure

Start with a simple root module for learning. Later, move the VPC logic into `/modules/vpc/` and call it from `/environments/dev/`, `/environments/staging/`, and `/environments/prod/`.

Simple Terraform AWS VPC folder structure
terraform-aws-vpc/
├── main.tf
├── providers.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── versions.tf
Module-ready folder structure
terraform/
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── versions.tf
└── environments/
    ├── dev/
    │   ├── main.tf
    │   ├── providers.tf
    │   ├── backend.tf
    │   ├── variables.tf
    │   └── terraform.tfvars
    └── prod/
        ├── main.tf
        ├── providers.tf
        ├── backend.tf
        ├── variables.tf
        └── terraform.tfvars
Full Terraform AWS VPC code

Full Terraform code to create AWS VPC

This example creates a VPC with two public subnets, two private subnets, an Internet Gateway, one NAT Gateway, public and private route tables, route table associations, and a simple security group.

versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
providers.tf
provider "aws" {
  region = var.aws_region
}
main.tf
data "aws_availability_zones" "available" {
  state = "available"
}

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-vpc"
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-igw"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-public-${count.index + 1}"
    Tier = "public"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-private-${count.index + 1}"
    Tier = "private"
  })
}

resource "aws_eip" "nat" {
  domain = "vpc"

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-nat-eip"
  })
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-nat-gateway"
  })

  depends_on = [aws_internet_gateway.main]
}

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

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-public-rt"
  })
}

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

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-private-rt"
  })
}

resource "aws_route_table_association" "public" {
  count = length(aws_subnet.public)

  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count = length(aws_subnet.private)

  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.environment}-web-sg"
  description = "Allow HTTP and HTTPS inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "Allow HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = var.allowed_http_cidrs
  }

  ingress {
    description = "Allow HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = var.allowed_https_cidrs
  }

  egress {
    description = "Allow outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-web-sg"
  })
}
Variables and outputs

Terraform variables and outputs

variables.tf
variable "aws_region" {
  description = "AWS region where the VPC will be created"
  type        = string
  default     = "us-east-1"
}

variable "project_name" {
  description = "Project name used for resource naming"
  type        = string
  default     = "cloudnetworking"
}

variable "environment" {
  description = "Environment name such as dev, staging, or prod"
  type        = string
  default     = "dev"
}

variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "CIDR blocks for public subnets"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "CIDR blocks for private subnets"
  type        = list(string)
  default     = ["10.0.11.0/24", "10.0.12.0/24"]
}

variable "allowed_http_cidrs" {
  description = "CIDR blocks allowed for HTTP inbound traffic"
  type        = list(string)
  default     = ["0.0.0.0/0"]
}

variable "allowed_https_cidrs" {
  description = "CIDR blocks allowed for HTTPS inbound traffic"
  type        = list(string)
  default     = ["0.0.0.0/0"]
}
terraform.tfvars
aws_region  = "us-east-1"
project_name = "cloudnetworking"
environment = "dev"

vpc_cidr = "10.0.0.0/16"

public_subnet_cidrs = [
  "10.0.1.0/24",
  "10.0.2.0/24"
]

private_subnet_cidrs = [
  "10.0.11.0/24",
  "10.0.12.0/24"
]
outputs.tf
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs of public subnets"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs of private subnets"
  value       = aws_subnet.private[*].id
}

output "nat_gateway_id" {
  description = "ID of the NAT Gateway"
  value       = aws_nat_gateway.main.id
}

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

output "web_security_group_id" {
  description = "ID of the web security group"
  value       = aws_security_group.web.id
}
Terraform commands

Terraform commands to deploy AWS VPC

1. Format Clean Terraform formatting.
2. Init Download AWS provider.
3. Validate Check syntax and structure.
4. Plan Preview VPC changes.
5. Apply Create AWS resources.
6. Destroy Remove lab resources.
Deploy commands
terraform fmt
terraform init
terraform validate
terraform plan
terraform apply
Destroy commands for lab cleanup
terraform destroy
NAT Gateway and Elastic IP can create ongoing AWS cost. For learning labs, destroy the resources after testing.
Terraform AWS VPC module

Convert this AWS VPC into a reusable Terraform module

After testing the VPC code, move it into a reusable module. This allows dev, staging, and production environments to call the same VPC pattern with different CIDR blocks, names, and tags.

Module call from environments/dev/main.tf
module "vpc" {
  source = "../../modules/vpc"

  project_name = "cloudnetworking"
  environment  = "dev"
  aws_region   = "us-east-1"

  vpc_cidr = "10.10.0.0/16"

  public_subnet_cidrs = [
    "10.10.1.0/24",
    "10.10.2.0/24"
  ]

  private_subnet_cidrs = [
    "10.10.11.0/24",
    "10.10.12.0/24"
  ]
}

Reusable

Use the same VPC module across multiple environments and AWS accounts.

Standardized

Control naming, tags, subnets, routing, and network foundations from one pattern.

Pipeline-friendly

Run plan and apply from CI/CD using environment-specific tfvars files.

Recommended internal link: connect this page to your existing Terraform Modules guide so users can learn reusable module design after building the VPC.
AWS VPC Terraform best practices

Best practices for Terraform AWS VPC

Plan CIDR carefully

Avoid overlapping CIDRs with on-prem, VPN, Transit Gateway, peering, or future VPC networks.

Use multiple AZs

Spread public and private subnets across at least two Availability Zones for high availability.

Separate route tables

Use different route tables for public and private subnets to avoid accidental exposure.

Use private subnets for workloads

Place app servers, databases, and Kubernetes worker nodes in private subnets where possible.

Use NAT carefully

For production, consider one NAT Gateway per AZ. For labs, one NAT Gateway reduces cost.

Tag everything

Use standard tags for ownership, environment, cost center, application, and compliance.

Use remote state

For teams, store Terraform state remotely using S3 backend with locking instead of local state.

Review every plan

Route table and subnet changes can impact connectivity, so always review Terraform plan carefully.

Modularize after testing

Start simple, test the VPC, then move it into a reusable module for environments.

Common mistakes

Common Terraform AWS VPC mistakes

Mistake Impact Better approach
Overlapping CIDR ranges Breaks future VPN, VPC peering, Transit Gateway, and hybrid networking. Plan IP ranges before creating the VPC.
Private subnet route to Internet Gateway Private workloads may become incorrectly exposed. Private subnet default route should point to NAT Gateway if outbound internet is needed.
No route table associations Subnets may use unintended default route tables. Explicitly associate route tables with subnets.
Using one AZ only Poor availability and weak production design. Use at least two Availability Zones.
Hardcoded values everywhere Code is difficult to reuse for dev, staging, and production. Use variables, locals, and tfvars files.
Leaving lab NAT Gateway running Unexpected AWS cost. Destroy lab resources after testing.
The most common VPC mistake is confusing public and private subnet routing. A public subnet needs a route to the Internet Gateway. A private subnet should not have a direct route to the Internet Gateway.
Interview and real-world explanation

How to explain Terraform AWS VPC in interviews

A strong interview answer should not only say “I created a VPC.” It should explain network design, subnet tiers, routing, security, high availability, and why Terraform is useful.

Sample interview answer
I created an AWS VPC using Terraform with a custom CIDR block, public and private subnets across multiple Availability Zones, an Internet Gateway for public subnets, a NAT Gateway for private subnet outbound access, separate route tables, route table associations, and security groups. I used variables for environment-specific values and outputs for VPC and subnet IDs. After testing, I moved the design into a reusable Terraform module so dev, staging, and production environments could use the same standard network pattern.

Beginner point

Know what VPC, subnet, route table, Internet Gateway, NAT Gateway, and security group mean.

Intermediate point

Explain why public and private subnets need different routes.

Advanced point

Discuss multi-AZ design, NAT Gateway cost, VPC endpoints, Transit Gateway, and reusable modules.

Official references

Official AWS and Terraform references

Use these official references for deeper details on AWS VPC, Terraform AWS provider resources, route tables, NAT Gateway, and VPC networking behavior.

AWS VPC Documentation

Official AWS documentation for Amazon Virtual Private Cloud concepts and configuration.

Open AWS VPC Docs →

AWS VPC User Guide

Official VPC user guide for subnets, routing, gateways, security, and connectivity.

Open VPC User Guide →

Terraform aws_vpc Resource

Official Terraform resource documentation for creating and managing AWS VPCs.

Open aws_vpc Docs →

Terraform Hub

Return to the main Terraform learning hub for architecture, workflow, state, providers, and modules.

Open Terraform Hub →