Skip to main content

VPC Networking from Scratch — Subnets, NAT, and Security Groups

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

A developer once asked me: "Why can't my Lambda function reach the internet after I put it in a VPC?" The answer took 15 minutes to explain. VPC networking is where most AWS engineers hit a wall — and it's because nobody taught them the fundamentals first.

CIDR Blocks — The Foundation of VPC Design

Every VPC needs a CIDR block that defines its IP address range. Think of it as the total pool of IP addresses available inside your virtual network.

CIDR BlockIP AddressesTypical Use
/16 (e.g., 10.0.0.0/16)65,536Production VPC (room to grow)
/20 (e.g., 10.0.0.0/20)4,096Medium applications
/24 (e.g., 10.0.0.0/24)256Small dev/test environments

Rule of thumb: Start with a /16 for production. You can't resize a VPC after creation, and running out of IP addresses is painful.

# Create a VPC with a /16 CIDR block
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=production-vpc}]' \
--query 'Vpc.VpcId' \
--output text)

echo "VPC created: $VPC_ID"

# Enable DNS hostnames (required for many AWS services)
aws ec2 modify-vpc-attribute \
--vpc-id $VPC_ID \
--enable-dns-hostnames '{"Value": true}'

Public vs Private Subnets

This is the single most important networking concept in AWS. The difference between public and private subnets is simple: a public subnet has a route to an Internet Gateway. A private subnet does not.

# Create public subnets (two AZs for high availability)
PUB_SUBNET_1=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-subnet-1a}]' \
--query 'Subnet.SubnetId' --output text)

PUB_SUBNET_2=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-subnet-1b}]' \
--query 'Subnet.SubnetId' --output text)

# Create private subnets
PRIV_SUBNET_1=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.10.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-subnet-1a}]' \
--query 'Subnet.SubnetId' --output text)

PRIV_SUBNET_2=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.11.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-subnet-1b}]' \
--query 'Subnet.SubnetId' --output text)

Internet Gateway — The Door to the Internet

An Internet Gateway (IGW) allows resources in public subnets to communicate with the internet.

# Create and attach an Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=production-igw}]' \
--query 'InternetGateway.InternetGatewayId' --output text)

aws ec2 attach-internet-gateway \
--internet-gateway-id $IGW_ID \
--vpc-id $VPC_ID

Route Tables — Traffic Directions

Route tables tell traffic where to go. Public subnets route 0.0.0.0/0 (everything) to the IGW. Private subnets route 0.0.0.0/0 to a NAT Gateway.

# Create a public route table
PUB_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=public-rt}]' \
--query 'RouteTable.RouteTableId' --output text)

# Add route to Internet Gateway
aws ec2 create-route \
--route-table-id $PUB_RT \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW_ID

# Associate public subnets with the public route table
aws ec2 associate-route-table --subnet-id $PUB_SUBNET_1 --route-table-id $PUB_RT
aws ec2 associate-route-table --subnet-id $PUB_SUBNET_2 --route-table-id $PUB_RT

# Enable auto-assign public IPs for public subnets
aws ec2 modify-subnet-attribute \
--subnet-id $PUB_SUBNET_1 \
--map-public-ip-on-launch

NAT Gateway — Private Subnet Internet Access

Resources in private subnets (databases, app servers) often need to reach the internet for updates, API calls, etc. A NAT Gateway provides outbound-only access — nothing from the internet can initiate a connection in.

# Allocate an Elastic IP for the NAT Gateway
EIP_ALLOC=$(aws ec2 allocate-address \
--domain vpc \
--query 'AllocationId' --output text)

# Create NAT Gateway in a public subnet
NAT_GW=$(aws ec2 create-nat-gateway \
--subnet-id $PUB_SUBNET_1 \
--allocation-id $EIP_ALLOC \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=production-nat}]' \
--query 'NatGateway.NatGatewayId' --output text)

# Wait for NAT Gateway to become available
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW

# Create private route table with NAT Gateway route
PRIV_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=private-rt}]' \
--query 'RouteTable.RouteTableId' --output text)

aws ec2 create-route \
--route-table-id $PRIV_RT \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id $NAT_GW

aws ec2 associate-route-table --subnet-id $PRIV_SUBNET_1 --route-table-id $PRIV_RT
aws ec2 associate-route-table --subnet-id $PRIV_SUBNET_2 --route-table-id $PRIV_RT

Cost alert: NAT Gateways cost ~$32/month plus $0.045/GB of data processed. For high-traffic workloads, consider NAT instances or VPC endpoints instead.

Security Groups vs NACLs

Both filter traffic, but they work very differently:

FeatureSecurity GroupsNACLs
LevelInstance (ENI) levelSubnet level
StateStateful (return traffic auto-allowed)Stateless (must allow return traffic explicitly)
RulesAllow rules onlyAllow and Deny rules
EvaluationAll rules evaluated togetherRules processed in order by number
DefaultDeny all inbound, allow all outboundAllow all inbound and outbound
Use CasePer-instance firewallSubnet-wide deny rules, compliance

In practice: use security groups for 90% of your firewall rules. Use NACLs only when you need to explicitly deny traffic (like blocking a specific IP range).

VPC Endpoints — Skip the Internet

When your private subnet resources need to access S3 or DynamoDB, don't route through NAT Gateway. Use VPC endpoints — they're free (for gateway type) and faster:

# Gateway endpoint for S3 (free)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids $PRIV_RT \
--vpc-endpoint-type Gateway

# Interface endpoint for Secrets Manager (costs ~$7.20/month per AZ)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.us-east-1.secretsmanager \
--subnet-ids $PRIV_SUBNET_1 $PRIV_SUBNET_2 \
--vpc-endpoint-type Interface \
--private-dns-enabled

VPC Flow Logs — See All Traffic

Flow logs capture metadata about IP traffic going to and from your VPC. Essential for troubleshooting connectivity issues and security auditing:

# Enable VPC flow logs to CloudWatch
aws ec2 create-flow-log \
--resource-type VPC \
--resource-id $VPC_ID \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flow-logs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/VPCFlowLogsRole

Flow logs tell you source IP, destination IP, port, protocol, action (ACCEPT/REJECT), and bytes transferred. If a connection is being rejected, flow logs show you exactly where and why.

What's Next?

You've built a complete VPC from scratch. Next up: AWS CLI like a pro — 30 commands that replace clicking through the console, with JMESPath queries and shell scripting tips.


This is Part 5 of our AWS series. Networking is the backbone of everything in the cloud — get it right once and everything else gets easier.