Skip to main content

Command Palette

Search for a command to run...

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

Updated
β€’7 min readβ€’View as Markdown
Demystifying AWS Networking (VPCs, Subnets, and Security Groups)
A
Hi, I'm Atul! I am a software engineer with a strong focus on DevOps, Cloud Infrastructure, and automation. I spend my time building scalable CI/CD pipelines, mastering tools like Docker, Terraform, and AWS, and breaking down complex cloud concepts into accessible, hands-on tutorials. I believe the best way to learn is to build, break, and document the process.

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.

Zero to Production with AWS ECS Fargate: Terraform, CI/CD, RDS, Monitoring

Part 1 of 1

A hands-on AWS ECS Fargate series that takes a containerized app from zero to production using Terraform, VPC networking, ECR, Application Load Balancer, blue-green deployments, CloudWatch monitoring, RDS PostgreSQL, Secrets Manager, and GitHub Actions CI/CD.

More from this blog

A

Atul Codes | DevOps & Cloud Engineering

15 posts

Welcome to Atul Codes! This blog is dedicated to helping developers master DevOps, Cloud Computing, and infrastructure automation. Expect weekly, hands-on tutorials covering CI/CD pipelines, Docker, AWS, and Terraform. Whether you are deploying your first container or looking to optimize your cloud architecture, you will find practical, step-by-step guides and real-world solutions here.