Demystifying AWS Networking (VPCs, Subnets, and Security Groups)

Introduction
Before deploying your first ECS Fargate cluster, you need to understand AWS networking fundamentals. Most beginners skip this and struggle with "Connection refused" errors later. This guide breaks down VPCs, subnets, and security groups in plain Englishβwith real diagrams you can replicate.
What You'll Learn:
β What is a VPC and why it's your app's "virtual data center"
β Public vs. Private subnets (and when to use each)
β Security Groups as virtual firewalls (with ECS Fargate examples)
β NAT Gateways for private subnet internet access
β Complete Terraform code to create production-ready networking
Prerequisites:
AWS account (free tier works)
Basic Terraform knowledge
Docker fundamentals
1. What is a VPC? (Virtual Private Cloud)
Think of a VPC as your own isolated cloud network inside AWS. It's like renting a private building in a massive mallβyou share the infrastructure but have complete control over your space.
Key VPC Components:
VPC = [CIDR Block: 10.0.0.0/16]
βββ Internet Gateway (connects to public internet)
βββ Subnets (10.0.1.0/24, 10.0.2.0/24, etc.)
βββ Route Tables (control traffic flow)
βββ Security Groups (firewall rules)
βββ NAT Gateway (private subnet internet access)
Why VPC Matters for ECS Fargate:
Your containers run inside subnets
Security Groups control who can access your app
Without proper routing, ECS can't pull from ECR or talk to RDS
2. Subnets: Public vs. Private (Critical for Production)
A subnet is a slice of your VPC where resources live. For ECS Fargate, you'll need both:
| Subnet Type | Purpose | ECS Fargate Use Case |
|---|---|---|
| Public Subnet | Has internet access via Internet Gateway | For services that need direct public access (ALB) |
| Private Subnet | Internet access via NAT Gateway only | For your ECS containers (secure, no direct public access) |
Real Example:
# Public Subnet (for Application Load Balancer)
resource "aws_subnet" "public" {
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 = "ecs-public-subnet"
}
}
# Private Subnet (for ECS Fargate containers)
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1a"
tags = {
Name = "ecs-private-subnet"
}
}
Why This Setup?
ALB (in public subnet) receives user requests
ECS containers (in private subnet) process requests securely
Hacker bonus: This is exactly what companies use in production!
3. Security Groups: Your Virtual Firewall
A Security Group (SG) is a stateful firewall that controls traffic to your resources. Unlike traditional firewalls, you only allow traffic (no deny rules).
Key Rules for ECS Fargate:
| Security Group | Inbound Rules | Outbound Rules | Purpose |
|---|---|---|---|
| ALB SG | TCP 80/443 from anywhere | TCP to ECS SG | Receives user requests |
| ECS SG | TCP from ALB SG only | All to RDS SG | Protected containers |
| RDS SG | TCP from ECS SG only | All | Database (never public) |
Terraform Code:
# ALB Security Group (public access)
resource "aws_security_group" "alb" {
name = "ecs-alb-sg"
vpc_id = aws_vpc.main.id
description = "Allow HTTP/HTTPS from anywhere"
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ecs-alb-security-group"
}
}
# ECS Security Group (protected)
resource "aws_security_group" "ecs" {
name = "ecs-app-sg"
vpc_id = aws_vpc.main.id
description = "Only allow traffic from ALB"
ingress {
description = "From ALB"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ecs-application-security-group"
}
}
Critical Tip: Notice how ECS SG only allows traffic from ALB SG (not from 0.0.0.0/0). This is production security!
4. NAT Gateway: Private Subnet Internet Access
Your private ECS containers need internet access to:
Pull Docker images from ECR
Download dependencies during app startup
Call external APIs (SendGrid, Slack, etc.)
But they shouldn't be publicly accessible. Solution: NAT Gateway.
Private Subnet β NAT Gateway (10.0.3.0/24) β Internet Gateway β Public Internet
Terraform:
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
tags = {
Name = "ecs-nat-gateway"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
}
# Update private subnet route table
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 = {
Name = "ecs-private-route-table"
}
}
5. Complete Terraform: Production-Ready VPC
Here's the full code you can copy-paste:
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "ecs-production-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "ecs-internet-gateway"
}
}
# Attach IGW to VPC
resource "aws_internet_gateway_attachment" "main" {
internet_gateway_id = aws_internet_gateway.main.id
vpc_id = aws_vpc.main.id
}
# Public Subnet
resource "aws_subnet" "public" {
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 = "ecs-public-subnet"
}
}
# Private Subnet
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1a"
tags = {
Name = "ecs-private-subnet"
}
}
# NAT Gateway + EIP
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
tags = {
Name = "ecs-nat-gateway"
}
}
# Route Tables
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 = {
Name = "ecs-public-route-table"
}
}
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 = {
Name = "ecs-private-route-table"
}
}
# Route Table Associations
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
# Security Groups (from earlier)
resource "aws_security_group" "alb" {
# ... (paste ALB SG code)
}
resource "aws_security_group" "ecs" {
# ... (paste ECS SG code)
}
Deployment:
terraform init
terraform plan
terraform apply
Expected Cost:
VPC: Free
NAT Gateway: ~$35/month (0.045/hr + data processing)
Subnets: Free
6. Common Mistakes to Avoid
| Mistake | Why It Breaks | Fix |
|---|---|---|
| ECS in public subnet | Containers exposed to public internet | Use private subnet |
| No NAT Gateway | ECS can't pull from ECR | Add NAT + route table |
SG allows 0.0.0.0/0 |
Security risk | Restrict to ALB SG only |
| DNS disabled | ECR/ECS can't resolve | Set enable_dns_hostnames = true |
Summary
You now have a production-ready VPC with:
β Public subnet (for ALB)
β Private subnet (for ECS Fargate)
β NAT Gateway (internet access for private)
β Security Groups (ALB β ECS β RDS chain)
Next Step: Part 2 will show you how to push Docker images to AWS ECR and configure authentication.



