Skip to content

NKCODE TECH GEEK ZONE

  • RSS - Posts
Menu
  • Home
  • Cloud
    • Azure
    • Alibaba
    • AWS
  • Hardware
  • Linux
  • Network
  • Security
  • Windows Client / Servers
    • SQL
    • Windows Client OS
      • Windows 10
    • Windows Servers
      • Windows 2008R2
      • Windows Server 2012R2
      • Windows Server 2016
      • Windows Server 2019
  • VMWARE
  • Free Tools
  • About Me
    • Disclaimer
Menu

How to Create an Amazon VPC with Public and Private Subnets from Scratch

Posted on April 2, 2026

Deploying infrastructure on AWS starts with networking. While AWS offers automated wizards to generate VPC topologies in a single click, configuring one manually teaches you how internet gateways, route tables, subnets, and NAT gateways interact under the hood.

In this guide, we will design and deploy a production-grade VPC featuring:

  • A /16 CIDR block for plenty of address space.

  • Two Public Subnets across two Availability Zones (AZs) for public-facing resources like load balancers or bastion hosts.

  • Two Private Subnets across two AZs for secure application servers and databases.

  • An Internet Gateway (IGW) for inbound/outbound internet routing.

  • A NAT Gateway allowing private instances to pull outbound updates safely without exposing them to incoming internet traffic.

Architecture Overview & IP Planning

Before touching the AWS Management Console, planning your CIDR blocks prevents overlapping IP conflicts later.

Resource CIDR Block Availability Zone Purpose
VPC 10.0.0.0/16 Multi-AZ Entire isolated network
Public Subnet 1 10.0.1.0/24 us-east-1a Public ingress / ALB / NAT
Public Subnet 2 10.0.2.0/24 us-east-1b Public ingress (High Availability)
Private Subnet 1 10.0.10.0/24 us-east-1a Application backend / microservices
Private Subnet 2 10.0.20.0/24 us-east-1b Application backend (High Availability)

Note: AWS reserves the first 4 IP addresses and the last IP address in every subnet (5 IPs total). A /24 subnet gives you 251 usable host IPs.

Step 1: Create the VPC

  1. Open the AWS Management Console and navigate to VPC.

  2. Click Create VPC.

  3. Under Resources to create, select VPC only (avoiding the automatic wizard).

  4. Configure the settings:

    • Name tag: production-vpc

    • IPv4 CIDR block: 10.0.0.0/16

    • Tenancy: Default

  5. Click Create VPC.

  6. Once created, select your new VPC, click Actions > Edit VPC settings, check both Enable DNS resolution and Enable DNS hostnames, and save changes.

Step 2: Create Public and Private Subnets

Navigate to Subnets in the left menu and click Create subnet. Select your production-vpc and create each of the following:

Public Subnets

  1. Public-Subnet-1A:

    • Availability Zone: us-east-1a

    • IPv4 CIDR block: 10.0.1.0/24

  2. Public-Subnet-1B:

    • Availability Zone: us-east-1b

    • IPv4 CIDR block: 10.0.2.0/24

Private Subnets

  1. Private-Subnet-1A:

    • Availability Zone: us-east-1a

    • IPv4 CIDR block: 10.0.10.0/24

  2. Private-Subnet-1B:

    • Availability Zone: us-east-1b

    • IPv4 CIDR block: 10.0.20.0/24

Enable Auto-Assign Public IPs for Public Subnets

By default, EC2 instances launched into a subnet will not receive a public IP address unless configured:

  1. Select Public-Subnet-1A.

  2. Click Actions > Edit subnet settings.

  3. Check Enable auto-assign public IPv4 address.

  4. Repeat this step for Public-Subnet-1B.

Step 3: Attach an Internet Gateway (IGW)

An Internet Gateway allows communication between your VPC and the outside internet.

  1. In the VPC dashboard, select Internet gateways > Create internet gateway.

  2. Name it production-igw and click Create internet gateway.

  3. On the confirmation screen, click Actions > Attach to VPC.

  4. Select production-vpc and confirm the attachment.

Step 4: Configure the Public Route Table

A subnet only becomes “public” when its route table points default outbound traffic (0.0.0.0/0) to an Internet Gateway.

  1. Navigate to Route tables > Create route table.

  2. Name: public-rt.

  3. Select production-vpc and click Create route table.

  4. Select public-rt, go to the Routes tab, and click Edit routes:

    • Click Add route.

    • Destination: 0.0.0.0/0

    • Target: Select Internet Gateway, then choose production-igw.

    • Click Save changes.

  5. Switch to the Subnet associations tab:

    • Click Edit subnet associations.

    • Select both Public-Subnet-1A and Public-Subnet-1B.

    • Click Save associations.

Step 5: Deploy a NAT Gateway for Private Subnets

Instances in your private subnets often need internet access (for downloading security updates, pulling packages, or hitting third-party APIs), but they must never accept unsolicited inbound connections.

  1. Navigate to NAT gateways > Create NAT gateway.

  2. Configure settings:

    • Name: production-nat-gw

    • Subnet: Select a Public Subnet (e.g., Public-Subnet-1A). A NAT Gateway must live in a public subnet to communicate with the internet.

    • Connectivity type: Public

    • Elastic IP allocation ID: Click Allocate Elastic IP to generate a dedicated static public IP.

  3. Click Create NAT gateway. (It takes 1–3 minutes to transition from Pending to Available).

Step 6: Configure the Private Route Table

Now, route private outbound traffic through the newly provisioned NAT Gateway.

  1. Navigate to Route tables > Create route table.

  2. Name: private-rt.

  3. Select production-vpc and click Create route table.

  4. Under the Routes tab, click Edit routes:

    • Click Add route.

    • Destination: 0.0.0.0/0

    • Target: Select NAT Gateway, then choose production-nat-gw.

    • Click Save changes.

  5. Under the Subnet associations tab:

    • Click Edit subnet associations.

    • Check Private-Subnet-1A and Private-Subnet-1B.

    • Click Save associations.

Step 7: Verification & Security Best Practices

To ensure everything is working:

  1. Launch a test EC2 instance in Public-Subnet-1A: Confirm it gets a public IP and you can SSH into it.

  2. Launch a test EC2 instance in Private-Subnet-1A: Verify it receives only a private IP (10.0.10.x).

  3. SSH from the Public instance to the Private instance using its private IP.

  4. Test Outbound Connectivity: From inside the private instance, run:

curl -I https://aws.amazon.com

If it returns HTTP/2 200, your NAT Gateway route is resolving correctly while keeping the instance completely shielded from public discovery.

Production Tips:

  • Cost Optimization: A NAT Gateway incurs an hourly charge plus per-gigabyte data processing fees. For non-production dev environments, delete the NAT Gateway when not in use.

  • High Availability: For true production redundancy across AZs, deploy one NAT Gateway per Availability Zone and associate each with its respective private subnet route table. This ensures that if us-east-1a fails, us-east-1b continues operating uninterrupted.

Here is the complete, modular Terraform configuration implementing the VPC architecture defined in the guide.

File Structure

vpc-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

variables.tf

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

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

variable "vpc_name" {
  description = "Base name applied to resource tags"
  type        = string
  default     = "production-vpc"
}

variable "availability_zones" {
  description = "List of Availability Zones to deploy subnets into"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b"]
}

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.10.0/24", "10.0.20.0/24"]
}

main.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# 1. VPC Definition
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = var.vpc_name
  }
}

# 2. Internet Gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.vpc_name}-igw"
  }
}

# 3. Public Subnets
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       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.vpc_name}-public-${var.availability_zones[count.index]}"
    Tier = "Public"
  }
}

# 4. Private Subnets
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       = var.availability_zones[count.index]
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.vpc_name}-private-${var.availability_zones[count.index]}"
    Tier = "Private"
  }
}

# 5. Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
  domain     = "vpc"
  depends_on = [aws_internet_gateway.gw]

  tags = {
    Name = "${var.vpc_name}-nat-eip"
  }
}

# 6. NAT Gateway (Placed in First Public Subnet)
resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name = "${var.vpc_name}-nat-gw"
  }

  depends_on = [aws_internet_gateway.gw]
}

# 7. Public Route Table & Routing
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

  tags = {
    Name = "${var.vpc_name}-public-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
}

# 8. Private Route Table & Routing
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.nat.id
  }

  tags = {
    Name = "${var.vpc_name}-private-rt"
  }
}

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
}

outputs.tf

output "vpc_id" {
  description = "The ID of the provisioned VPC"
  value       = aws_vpc.main.id
}

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

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

output "nat_gateway_ip" {
  description = "Public Elastic IP assigned to the NAT Gateway"
  value       = aws_eip.nat.public_ip
}

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

terraform.tfvars

aws_region           = "us-east-1"
vpc_name             = "production-vpc"
vpc_cidr             = "10.0.0.0/16"
availability_zones   = ["us-east-1a", "us-east-1b"]
public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]

Execution

terraform init
terraform plan
terraform apply

Share this:

  • Share on X (Opens in new window) X
  • Share on Facebook (Opens in new window) Facebook
  • Share on LinkedIn (Opens in new window) LinkedIn
  • Share on Pinterest (Opens in new window) Pinterest
  • Share on Telegram (Opens in new window) Telegram
  • Share on WhatsApp (Opens in new window) WhatsApp
  • Share on Reddit (Opens in new window) Reddit
  • Email a link to a friend (Opens in new window) Email

Like this:

Like Loading…

Related

Welcome to Teck Geek Zone

Alibaba & Azure Cloud with a free trial worth $200-1200 USD Click below Cloud Providers

  • Integrating VMware Avi into Purdue and NIST Frameworks for the Marine Industry
  • Securing the Industrial Edge: A Guide to Microsoft Defender for IoT
  • IACS UR E26 and E27 Guidance: Maritime Cybersecurity Framework for Modern Ships
  • Understanding OT, ICS, and SCADA: A Complete Guide to Industrial Technology and Cybersecurity
  • Inside Microsoft Entra: Latest Security Innovations and Features

Categories

  • Cloud (203)
    • Alibaba (39)
    • AWS (46)
    • Azure (124)
  • Free Tools (5)
  • Hardware (17)
  • Linux (13)
  • Network (30)
  • Security (23)
  • VMWARE (59)
  • Windows OS (44)
    • Windows 10 (7)
  • Windows Servers (69)
    • SQL (3)
    • Windows 2008R2 (7)
    • Windows Server 2012R2 (15)
    • Windows Server 2016 (20)
    • Windows Server 2019 (10)

Subscribe to our newsletter

©2026 NKCODE TECH GEEK ZONE | Design: Newspaperly WordPress Theme
Loading Comments...
%d