# AWS Networking Basics For Developers

> **What this blog covers:** Building a VPC from scratch, understanding every component, calculating CIDR ranges, and connecting everything together securely.

## What is a VPC?

A **Virtual Private Cloud (VPC)** is your own isolated network inside AWS. Everything you deploy — EC2 instances, RDS databases, Lambda functions — lives inside a VPC.  

![](https://cdn.hashnode.com/uploads/covers/617143b30c5e175a1c612899/f63621b9-53d2-468d-89aa-8bc6642f470a.png align="center")

**Key facts:**

*   One VPC per region (but you can have multiple VPCs)
    
*   You define its IP address space using CIDR notation
    
*   By default it's completely isolated — no internet access until you configure it
    
*   Free to create (you pay for resources inside it, not the VPC itself)
    

* * *

## CIDR Notation — How IP Ranges Work

CIDR stands for **Classless Inter-Domain Routing**. It's how you define a block of IP addresses.

### The Format

```plaintext
10.0.0.0 / 16
─────────
IP Address  Prefix length (how many bits are "fixed")
```

An IPv4 address is **32 bits** total, written as 4 groups of 8 bits (octets):

```plaintext
10   .   0   .   0   .   0
│      │        │       │
8 bits  8 bits  8 bits  8 bits  =  32 bits total
```

### The Prefix Length Controls How Many IPs You Get

The prefix `/N` means the **first N bits are fixed** (your network). The **remaining (32 - N) bits are free** (your hosts).

```plaintext
Number of usable IPs = 2^(32 - N)
```

| CIDR | Fixed bits | Free bits | Total IPs | Usable IPs | Typical use |
| --- | --- | --- | --- | --- | --- |
| /8 | 8 | 24 | 16,777,216 | 16,777,214 | Huge enterprise |
| /16 | 16 | 16 | 65,536 | 65,534 | Large VPC |
| /20 | 20 | 12 | 4,096 | 4,094 | Medium VPC |
| /24 | 24 | 8 | 256 | 254 | Typical subnet |
| /26 | 26 | 6 | 64 | 62 | Small subnet |
| /28 | 28 | 4 | 16 | 14 | Tiny subnet |
| /32 | 32 | 0 | 1 | 1 | Single IP address |

> **Why "usable IPs" is always 2 less than total:** AWS reserves 5 IPs per subnet (first 4 + last 1). So a /24 gives 256 - 5 = **251 usable IPs**.

### Visual: What /16 vs /24 Looks Like

```plaintext
10.0.0.0/16  →  covers  10.0.0.0  to  10.0.255.255
               fixed: [10.0]  free: [0-255].[0-255]

10.0.1.0/24  →  covers  10.0.1.0  to  10.0.1.255
               fixed: [10.0.1]  free: [0-255]
```

### The 5 Reserved IPs AWS Takes (per subnet)

For a subnet `10.0.1.0/24`:

| IP Address | Reserved for |
| --- | --- |
| 10.0.1.0 | Network address |
| 10.0.1.1 | VPC router |
| 10.0.1.2 | AWS DNS |
| 10.0.1.3 | AWS future use |
| 10.0.1.255 | Broadcast address |

So **251 IPs** are actually available for your resources.

* * *

## How to Calculate IPs for Your VPC and Subnets

### Step-by-step planning

*   **Rule #1 — VPC must be bigger than all subnets combined**
    
*   **Rule #2 — Subnet CIDR must be within the VPC CIDR range**
    
*   **Rule #3 — Subnets in the same VPC cannot overlap**
    

### Example: Planning a production VPC

**Goal:** 2 public subnets + 2 private subnets across 2 Availability Zones

![](https://cdn.hashnode.com/uploads/covers/617143b30c5e175a1c612899/b4991ab4-85b4-4a2a-9143-1a4de769c0c3.png align="center")

**Why 10.0.1.0 and not 10.0.0.0 for subnets?** Leave `10.0.0.0/24` as a spare block. Start subnets at `.1`, `.2`, `.3`, `.4` so your numbering is clean and you have room to add subnets later.

### The CIDR Calculation Formula

```plaintext
First IP of subnet:  your chosen start address
Last IP of subnet:   first IP + (2^free_bits - 1)

Example: 10.0.3.0/24
  free bits = 32 - 24 = 8
  total IPs = 2^8 = 256
  range: 10.0.3.0 → 10.0.3.255
```

### Quick sizing guide

| Need | Use this | IPs available |
| --- | --- | --- |
| VPC (large) | /16 | 65,531 |
| VPC (medium) | /20 | 4,091 |
| Subnet (standard) | /24 | 251 |
| Subnet (medium) | /22 | 1,019 |
| Subnet (small) | /26 | 59 |
| Single resource | /32 | 1 |

### Private IP ranges you must use (RFC 1918)

AWS VPCs must use private IP ranges — these are NOT routable on the public internet:

```plaintext
10.0.0.0    –  10.255.255.255   →  /8  (16M IPs)
172.16.0.0  –  172.31.255.255   →  /12 (1M IPs)
192.168.0.0 –  192.168.255.255  →  /16 (65K IPs)
```

> **Best practice:** Use `10.0.0.0/16` for your VPC CIDR. It's the most spacious and clearest for mental mapping.

* * *

## Step 1 — Create the VPC

### What it is

The outer boundary of your private network in AWS. All subnets, gateways, and resources live inside it.

### How to create it

**AWS Console:**

1.  Go to VPC → Your VPCs → Create VPC
    
2.  Choose "VPC only" (not VPC and more — we want manual control)
    
3.  Set the IPv4 CIDR block
    

**Configuration:**

```plaintext
Name:        my-production-vpc
IPv4 CIDR:   10.0.0.0/16
Tenancy:     Default  (shared hardware — cheaper)
```

### What gets created automatically

*   A default route table (main route table)
    
*   A default security group
    
*   A default network ACL
    

> ⚠️ **Don't use the default VPC for production.** It has permissive defaults and a fixed CIDR. Always create a custom VPC.

* * *

## Step 2 — Create Subnets

### What they are

Subnets carve your VPC CIDR into smaller chunks, each tied to a specific **Availability Zone**. A subnet is "public" only if its route table has a route to an Internet Gateway — not because of its name.

```plaintext
VPC: 10.0.0.0/16
Public Subnet A  (10.0.1.0/24)  [us-east-1a]  ← has route to IGW
Public Subnet B  (10.0.2.0/24)  [us-east-1b]  ← has route to IGW
Private Subnet A (10.0.3.0/24)  [us-east-1a]  ← no route to IGW
Private Subnet B (10.0.4.0/24)  [us-east-1b]  ← no route to IGW
```

### How to create them

For each subnet, go to VPC → Subnets → Create Subnet:

```plaintext
Subnet 1 — Public A
  Name:   public-subnet-a
  VPC:    my-production-vpc
  AZ:     us-east-1a
  CIDR:   10.0.1.0/24

Subnet 2 — Public B
  Name:   public-subnet-b
  VPC:    my-production-vpc
  AZ:     us-east-1b
  CIDR:   10.0.2.0/24

Subnet 3 — Private A
  Name:   private-subnet-a
  VPC:    my-production-vpc
  AZ:     us-east-1a
  CIDR:   10.0.3.0/24

Subnet 4 — Private B
  Name:   private-subnet-b
  VPC:    my-production-vpc
  AZ:     us-east-1b
  CIDR:   10.0.4.0/24
```

### Enable auto-assign public IP (public subnets only)

For each public subnet: Actions → Edit Subnet Settings → Enable auto-assign public IPv4 address

> **Why two AZs?** If one AZ goes down (rare but it happens), your service keeps running from the other AZ. This is **high availability**.

* * *

## Step 3 — Internet Gateway (IGW)

### What it is

The door between your VPC and the internet. Without it, nothing in your VPC can reach the outside world — even resources in "public" subnets.

```plaintext
Internet
  │
[Internet Gateway]  ← attached to VPC
  │
[VPC]
```

### How it works

*   **Two-way:** allows inbound AND outbound internet traffic
    
*   **Stateless at the gateway level** (Security Groups handle stateful inspection)
    
*   **Horizontally scalable:** AWS manages it — no bandwidth limits, no single point of failure
    
*   **One per VPC**
    

### How to create and attach it

1.  VPC → Internet Gateways → Create Internet Gateway
    
2.  Name it `my-igw`
    
3.  After creation: Actions → Attach to VPC → select `my-production-vpc`
    

> ⚠️ **Two-step process!** Creating the IGW is not enough. You must explicitly **attach** it to your VPC. Then you must add a **route** pointing to it in your public subnet route table.

* * *

## Step 4 — Route Tables

### What they are

A route table is a set of rules that tells traffic "where to go." Every subnet must be associated with exactly one route table.

### The two route tables you need

**Public Route Table** (for public subnets):

```plaintext
Destination      Target
10.0.0.0/16      local      ← talk to any resource in the VPC
0.0.0.0/0        igw-xxxxx  ← everything else → internet
```

**Private Route Table** (for private subnets):

```plaintext
Destination      Target
10.0.0.0/16      local      ← talk to any resource in the VPC
0.0.0.0/0        nat-xxxxx  ← everything else → NAT Gateway (outbound only)
```

### How to create them

**Create public route table:**

1.  VPC → Route Tables → Create Route Table
    
2.  Name: `public-rt`, VPC: `my-production-vpc`
    
3.  Routes tab → Edit routes → Add route: `0.0.0.0/0` → Target: Internet Gateway → `my-igw`
    
4.  Subnet Associations tab → Associate `public-subnet-a` and `public-subnet-b`
    

**Create private route table:**

1.  Create Route Table: `private-rt`, VPC: `my-production-vpc`
    
2.  Add route: `0.0.0.0/0` → Target: NAT Gateway (do this after creating the NAT GW)
    
3.  Associate `private-subnet-a` and `private-subnet-b`
    

### Route table flow diagram

```plaintext
Incoming request from internet:
  0.0.0.0/0 → [IGW] → [Public subnet] → ALB → EC2 in private subnet

EC2 making outbound call:
  EC2 → [Private subnet route table] → 0.0.0.0/0 → [NAT GW] → [IGW] → Internet
  Response comes back through NAT GW → EC2 (NAT handles translation)
```

* * *

## Step 5 — NAT Gateway

### What it is

NAT = Network Address Translation. The NAT Gateway gives private subnet resources **one-way internet access** — they can initiate outbound connections, but the internet cannot initiate connections to them.

```plaintext
Private EC2 (10.0.3.10)
    │ wants to reach api.github.com
    
NAT Gateway (in public subnet, has Elastic IP: 54.x.x.x)
    │ translates 10.0.3.10 → 54.x.x.x
   
Internet Gateway
    │
    
api.github.com
    │ response goes back to 54.x.x.x (Elastic IP)
    
NAT Gateway translates back → delivers to 10.0.3.10

⛔ Someone trying to reach 10.0.3.10 FROM the internet? Blocked.
```

### Where it lives

**In the public subnet** — it needs internet access itself to forward traffic out.

### How to create it

1.  VPC → NAT Gateways → Create NAT Gateway
    
2.  Subnet: `public-subnet-a` (must be public!)
    
3.  Connectivity: Public
    
4.  Elastic IP: Allocate Elastic IP (click the button)
    
5.  Create
    

Then add to your private route table:

```plaintext
0.0.0.0/0  →  nat-xxxxxxxx
```

### Cost warning

NAT Gateway costs ~$0.045/hour + $0.045/GB data. For dev environments, consider a **NAT Instance** (cheaper but requires manual management).

* * *

## Step 6 — Security Groups

### What they are

Virtual firewalls attached to **individual resources** (EC2, RDS, ALB, etc.). They control what traffic is allowed in (inbound) and out (outbound).

### Key properties

| Property | Detail |
| --- | --- |
| Stateful | Allow inbound → response automatically allowed out |
| Allow-only | No deny rules — only allow rules |
| Default | All inbound DENIED, all outbound ALLOWED |
| Scope | Attached per resource, not per subnet |
| Multiple | One resource can have up to 5 security groups |

### Stateful explained

```plaintext
Client → [HTTPS port 443, ALLOWED] → EC2
EC2   → [Response traffic]         → Client  ✅ automatic, no rule needed

Compare with NACL (stateless):
EC2   → [Response on port 52341]   → Client  ❌ must explicitly allow ephemeral ports
```

### The 3-tier Security Group pattern

```plaintext
Internet
    │  (HTTPS 443)
   
[SG-ALB]  →  inbound: 443 from 0.0.0.0/0
              outbound: 8080 to SG-App
    │  (HTTP 8080)
   
[SG-App]  →  inbound: 8080 from SG-ALB
              outbound: 3306 to SG-DB
    │  (MySQL 3306)
   
[SG-DB]   →  inbound: 3306 from SG-App
              outbound: all (default)
```

### How to create Security Groups

**SG-ALB (Load Balancer):**

```plaintext
Name: sg-alb
VPC:  my-production-vpc

Inbound rules:
  Type: HTTPS  Port: 443   Source: 0.0.0.0/0   (internet traffic)
  Type: HTTP   Port: 80    Source: 0.0.0.0/0   (redirect to HTTPS)

Outbound rules:
  Type: Custom TCP  Port: 8080  Destination: sg-app  (to app servers)
```

**SG-App (EC2 App Servers):**

```plaintext
Name: sg-app
VPC:  my-production-vpc

Inbound rules:
  Type: Custom TCP  Port: 8080  Source: sg-alb  ← reference by SG ID!

Outbound rules:
  Type: MySQL/Aurora  Port: 3306  Destination: sg-db
  Type: HTTPS         Port: 443   Destination: 0.0.0.0/0  (for API calls via NAT)
```

**SG-DB (RDS Database):**

```plaintext
Name: sg-db
VPC:  my-production-vpc

Inbound rules:
  Type: MySQL/Aurora  Port: 3306  Source: sg-app  ← only app servers can reach it!

Outbound rules:
  (leave default — all outbound allowed)
```

### Why reference SGs by ID instead of IP?

```plaintext
If you used IP:   Source = 10.0.3.15
                  → scale to 10 EC2s = update rule 10 times

If you used SG:   Source = sg-app
                  → any EC2 with sg-app attached = always works
                  → scale to 100 EC2s = no changes needed ✅
```

* * *

## Step 7 — Network ACLs (Extra Layer)

### What they are

Network ACLs (NACLs) are **subnet-level firewalls**. They're stateless (must allow return traffic explicitly) and support both allow and deny rules.

### SG vs NACL comparison

| Feature | Security Group | Network ACL |
| --- | --- | --- |
| Level | Resource (EC2, RDS...) | Subnet |
| State | Stateful | Stateless |
| Rules | Allow only | Allow AND Deny |
| Rule evaluation | All rules at once | In order (lowest wins) |
| Default | Deny all inbound | Allow all in and out |
| Use for | Primary defense | Extra layer / IP blocks |

### Private subnet NACL example

```plaintext
Inbound rules:
  Rule 100  ALLOW  TCP  10.0.0.0/16  all ports  ← VPC-internal traffic only
  Rule *    DENY   ALL  0.0.0.0/0               ← block everything else

Outbound rules:
  Rule 100  ALLOW  TCP  10.0.0.0/16  all ports  ← VPC-internal
  Rule 200  ALLOW  TCP  0.0.0.0/0   1024-65535  ← ephemeral ports for responses
  Rule *    DENY   ALL  0.0.0.0/0               ← block everything else
```

> **Ephemeral ports (1024–65535):** When a server responds to a client, it uses a random high port. Since NACLs are stateless, you must explicitly allow these return ports in your outbound rules.

* * *

## Full Architecture — How It All Connects

![](https://cdn.hashnode.com/uploads/covers/617143b30c5e175a1c612899/32e19fae-4a65-4b9c-93e0-d97c94347cd4.png align="center")

```plaintext
Traffic flow (inbound request):
  Internet → IGW → ALB (public subnet) → EC2 (private subnet) → RDS (private subnet)

Traffic flow (outbound from EC2):
  EC2 → Private route table → NAT GW (public subnet) → IGW → Internet
```

### Request lifecycle step by step

```plaintext
1. User hits https://myapp.com
        │
2. DNS resolves to ALB's public IP
        │
3. Internet → IGW → ALB (SG-ALB allows port 443 from 0.0.0.0/0)
        │
4. ALB → EC2 (SG-App allows port 8080 from SG-ALB)
        │
5. EC2 → RDS (SG-DB allows port 3306 from SG-App)
        │
6. RDS returns data → EC2 → ALB → User

[EC2 calling external API]
7. EC2 → private route table → 0.0.0.0/0 → NAT GW → IGW → api.example.com
8. Response → IGW → NAT GW → EC2  (internet can't initiate step 7 in reverse)
```

* * *

## Security Group Cheat Sheet

| Resource | Inbound | Source |
| --- | --- | --- |
| Load Balancer | 443 (HTTPS), 80 (HTTP) | 0.0.0.0/0 |
| EC2 app server | 8080 (or your app port) | SG-ALB |
| RDS MySQL | 3306 | SG-App |
| RDS Postgres | 5432 | SG-App |
| Redis | 6379 | SG-App |
| Bastion host | 22 (SSH) | Your IP only |
| EC2 via Bastion | 22 (SSH) | SG-Bastion |

* * *

## Common Mistakes

❌ Subnet is "public" because I named it public

✅ A subnet is public ONLY if its route table routes 0.0.0.0/0 to an IGW

❌ I created the IGW but resources still can't reach the internet

✅ Did you: (1) attach IGW to VPC? (2) add the route in the route table?

❌ My private EC2 can't download packages

✅ Check: (1) NAT Gateway exists? (2) private route table has 0.0.0.0/0 → NAT? (3) NAT Gateway is in a PUBLIC subnet?

❌ I set SG inbound source to the app server's IP address

✅ Set source to the SG ID (sg-xxxxxxxx) — it scales automatically

❌ NACL is blocking my traffic even though SG allows it

✅ NACLs are stateless — add outbound rule for ephemeral ports 1024-65535

❌ My subnets overlap (10.0.1.0/24 and 10.0.1.128/24)

✅ AWS will reject this. Plan CIDR ranges before creating subnets.

❌ I used /32 for my VPC CIDR

✅ VPC minimum is /28, maximum is /16. Recommended: /16 or /20.

* * *

### NAT Gateway MUST live in a public subnet

Here's the core logic: the NAT Gateway's entire job is to **forward your private resources' traffic out to the internet**. To do that, it needs internet access itself. And to have internet access, it must be in a subnet whose route table points to the Internet Gateway.

That's the definition of a public subnet.

```plaintext
Private EC2 wants to reach github.com
        │
       
Private route table: 0.0.0.0/0 → NAT Gateway
        │
       
NAT Gateway (sitting in PUBLIC subnet, has Elastic IP)
        │
       
Public route table: 0.0.0.0/0 → IGW
        │
       
Internet Gateway → github.com
```

If you put the NAT Gateway in a **private subnet**, it would have no route to the internet itself — so it could never forward traffic anywhere. It would be completely useless, like a postal relay office with no road connecting it to the outside world.

## What makes the NAT Gateway "safe" even though it's public

People get nervous hearing "NAT Gateway is in the public subnet" — but here's why it's not a security risk:

The NAT Gateway **does not have a Security Group**. It's not a compute resource you SSH into. It's a managed AWS appliance that only does one thing: translate source IPs. It never accepts unsolicited inbound connections from the internet — it only handles return traffic for connections that your private resources initiated.

The protection of your private EC2 and RDS instances comes from the fact that they have **no public IP** and **no route from the internet reaching them**. The NAT Gateway is just a forwarding pipe — it's not the thing being protected.

```plaintext
Internet trying to reach your private EC2:
  Internet → IGW → ... no route to 10.0.3.x exists in public subnet → DROPPED

Your private EC2 calling the internet:
  EC2 → NAT GW → IGW → Internet → response back to NAT GW → EC2  ✅
```

## One more practical tip

You should create **one NAT Gateway per Availability Zone**, not one total. If you have subnets in us-east-1a and us-east-1b, create a NAT Gateway in each public subnet, then point each private subnet's route table to the NAT Gateway in the same AZ. If you share a single NAT Gateway across AZs and that AZ goes down, all your private instances lose internet access simultaneously — which defeats the whole purpose of multi-AZ architecture.
