Guides/AWSAWS/AWS VPC and Networking Basics

AWS VPC and Networking Basics

The VPC mental model that makes AWS networking click: CIDR and subnets, what actually makes a subnet public, gateways, route tables, security groups vs NACLs, and endpoints.


AWS networking scares people because it looks like a wall of acronyms - VPC, IGW, NAT, NACL, route tables, security groups - with no obvious center. But there is a center, and it is small: a VPC is just your own private network inside AWS, and almost every piece is either an address range, a rule about which addresses can reach which, or a gateway that lets traffic in or out. Once you see it as one network you control, the acronyms stop being trivia and become the handful of levers you actually pull. This guide builds that mental model, then walks a real packet from the internet all the way to a private instance and back.

The one idea: a VPC is your own private network

A VPC (Virtual Private Cloud) is a logically isolated network that belongs to you, inside an AWS region. Nothing gets in or out of it unless you explicitly wire up a path. That is the whole frame: you are handed an empty, private network, and everything else in this guide is you deciding what addresses live in it, how it is carved up, and which doors exist to the outside world.

Two things anchor a VPC. First, it lives in one region and spans all the Availability Zones (AZs) in that region - separate physical datacenters you place resources across for fault tolerance. Second, it owns a block of private IP addresses that you choose up front. Everything you launch (EC2 instances, load balancers, databases) gets an address from that block and talks to everything else in the VPC as if they were on the same LAN.

By default, instances in a VPC can reach each other privately, and nothing else. No internet, no other VPCs, no on-prem network - not until you add a gateway and a route. That "closed by default" posture is the point. You open exactly the paths you need and no more.

CIDR blocks and subnets

When you create a VPC you give it a CIDR block - a range of IP addresses in slash notation, like 10.0.0.0/16. The number after the slash is how many bits are fixed as the network prefix; the rest are free for hosts. A /16 fixes the first 16 bits (10.0.x.x), leaving 16 bits of host space - about 65,536 addresses. A /24 fixes 24 bits (10.0.0.x), leaving 256. Smaller slash number means bigger network.

10.0.0.0/16   -> 10.0.0.0   to 10.0.255.255   (~65,536 addresses)
10.0.1.0/24   -> 10.0.1.0   to 10.0.1.255     (256 addresses)
10.0.2.0/24   -> 10.0.2.0   to 10.0.2.255     (256 addresses)

Use the private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) so your addresses never collide with the public internet. A common, safe starting point is a /16 for the VPC, giving you plenty of room to slice.

You do not launch things directly into the VPC's big block. You carve it into subnets - smaller CIDR ranges that each live in exactly one AZ. A subnet is where instances actually get placed, and putting subnets in different AZs is how you spread a workload so one datacenter failure does not take you down.

VPC:        10.0.0.0/16
  subnet A: 10.0.1.0/24   in us-east-1a
  subnet B: 10.0.2.0/24   in us-east-1b
  subnet C: 10.0.3.0/24   in us-east-1a
  subnet D: 10.0.4.0/24   in us-east-1b

Each subnet's range must fit inside the VPC's range and not overlap any other subnet. Note that AWS reserves 5 addresses in every subnet (the first four and the last), so a /24 gives you 251 usable, not 256. Beyond that, a subnet is just an address range in one AZ - it is not "public" or "private" by nature. What makes it one or the other is routing, which is the next piece.

Public vs private subnets: what actually makes a subnet public

This is the single most misunderstood thing in AWS networking, so be precise about it: a subnet is public only because its route table has a route to an internet gateway. There is no "public" checkbox that does the real work. The label is a description of the routing, not a property you set.

  • A public subnet has a route table entry sending internet-bound traffic (0.0.0.0/0) to an internet gateway. Instances in it can be reached from and reach the internet (if they also have a public IP).
  • A private subnet has no such route. Its traffic to 0.0.0.0/0 either goes nowhere or goes to a NAT gateway for outbound-only access. Nothing on the internet can initiate a connection to it.

Two instances, identical in every way, sitting in two subnets with the same size CIDR, are "public" or "private" purely based on where their subnet's route table points 0.0.0.0/0. Change the route, and you change which category the subnet is in. Internalize this and half the confusion disappears - when someone asks "why can't my instance reach the internet," the answer is almost always in the route table.

A second requirement for actually reaching the internet from a public subnet: the instance needs a public IP (or an Elastic IP). A route to the internet gateway is necessary but not sufficient; the instance also needs a public-facing address for return traffic to find it.

The internet gateway

An internet gateway (IGW) is the door between your VPC and the public internet. It is a horizontally scaled, redundant AWS-managed component - you create one, attach it to your VPC, and that is it; there is nothing to size or worry about failing.

The IGW does two jobs. It gives instances with public IPs a path to the internet, and it performs the network address translation between an instance's public IP and its private IP so packets route correctly in both directions. One IGW per VPC, and it only becomes useful once a route table actually points traffic at it.

Public subnet route table:
  10.0.0.0/16   -> local          (traffic within the VPC stays internal)
  0.0.0.0/0     -> igw-0abc123     (everything else goes to the internet gateway)

That local route is present in every route table automatically and cannot be removed - it is what lets everything in the VPC talk to everything else. The line that matters is the second one: send 0.0.0.0/0 (all non-local traffic) to the IGW, and this subnet is public.

NAT gateway: outbound for private subnets

Private subnets have a real problem. Your application servers and databases should not be reachable from the internet - that is the whole reason they are private - but they still need to reach out: to download OS updates, pull packages, call third-party APIs. They need outbound internet access without inbound.

A NAT gateway solves exactly this. You place it in a public subnet (it needs its own path to the IGW), and you point your private subnet's 0.0.0.0/0 route at the NAT gateway instead of the IGW. Now private instances can initiate connections outward - the NAT gateway translates their private source addresses to its own public address, sends the traffic out through the IGW, and routes the responses back. But because the private instances have no public IP and nothing on the internet knows how to address them, no one can start a connection into them. Outbound yes, inbound no.

Private subnet route table:
  10.0.0.0/16   -> local
  0.0.0.0/0     -> nat-0def456     (outbound internet via the NAT gateway)

Now the cost warning, because this catches people and shows up on real bills. A NAT gateway is not free. AWS charges you an hourly rate for it running (roughly $0.045/hour, so about $32/month) plus a per-gigabyte data-processing charge on everything that flows through it. Two traps follow from this. First, for high availability you want one NAT gateway per AZ (a NAT gateway lives in a single AZ, and if that AZ's gateway is unreachable, private subnets routed to it lose outbound access), which multiplies the hourly cost. Second, the per-GB charge means pulling large amounts of data out through a NAT gateway - big container images, backups, chatty S3 access - can quietly cost more than the servers themselves. The fix for the S3 and DynamoDB case is a VPC endpoint, covered below, which routes that traffic around the NAT gateway entirely.

Route tables tie it together

By now the pattern is clear: routing is what actually decides everything. A route table is a set of rules that says "for traffic headed to this destination CIDR, send it to this target." Every subnet is associated with exactly one route table, and that association is what makes the subnet public, private, or isolated.

A route table entry is a destination -> target pair. AWS evaluates them by most specific match wins (longest prefix), so a route for 10.0.5.0/24 beats the catch-all 0.0.0.0/0 for any address in that range.

Destination     Target          Meaning
10.0.0.0/16     local           stay inside the VPC (always present)
0.0.0.0/0       igw-0abc123     public subnet: everything else to the internet
Destination     Target          Meaning
10.0.0.0/16     local
0.0.0.0/0       nat-0def456     private subnet: outbound only, via NAT

The complete picture is just which route table each subnet points at. Public subnets share a route table with an IGW route. Private subnets share one with a NAT (or no 0.0.0.0/0 route at all, for fully isolated subnets like a database tier that never needs the internet). Nothing more mysterious than that - the "type" of a subnet is a consequence of its route table.

Security groups vs network ACLs

Routing decides where traffic can flow. Security groups and network ACLs decide what is allowed to flow. They are two separate firewalls at two different levels, and the exam-favorite distinctions between them are also the ones that bite you in real operations.

Security groups operate at the instance level (technically, the elastic network interface). They are:

  • Stateful - if you allow an inbound request, the response is automatically allowed back out, no matter your outbound rules. You think in terms of "what connections may start," and the return traffic is handled for you.
  • Allow-only - you can only write allow rules. There is no such thing as a deny rule in a security group; anything not explicitly allowed is simply not permitted.
  • Attached to instances, and you can reference other security groups as sources. "Allow the web tier's security group to reach the database on 5432" is a rule you write directly, no IP addresses needed - which is how the two-tier layout below stays clean as instances come and go.
Web security group (on the load balancer / web instances):
  Inbound:  allow TCP 443 from 0.0.0.0/0     (HTTPS from anywhere)
  Inbound:  allow TCP 80  from 0.0.0.0/0     (HTTP, usually redirected)

App/DB security group (on the private instances):
  Inbound:  allow TCP 5432 from web-security-group   (only from the web tier)

Network ACLs operate at the subnet level. They are:

  • Stateless - they do not remember connections. If you allow inbound traffic, you must separately allow the outbound response, and vice versa. This is the classic gotcha: you open inbound 443 but forget that responses leave from high-numbered ephemeral ports (1024-65535), so replies get dropped and you spend an hour confused.
  • Allow and deny - unlike security groups, NACLs support explicit deny rules, evaluated in numbered order. This is what makes them useful for a hard block, like denying a specific malicious IP range for an entire subnet.

So when do you reach for each? Security groups are your everyday tool - the primary control, applied per instance or per tier, and where nearly all your real access rules live. Network ACLs are a coarse, subnet-wide backstop. Most teams leave the NACLs at their default "allow all" and do all real work in security groups, dropping to NACLs only for blanket rules that must apply to an entire subnet (an explicit IP deny, or belt-and-suspenders isolation of a sensitive subnet). The two layers stack: traffic must pass the subnet's NACL and the instance's security group to get through.

A typical two-tier layout

Put the pieces together and you get the layout you will see in nearly every production AWS account. The goal is simple: expose only what must be exposed, keep the app and data private, and survive the loss of an Availability Zone.

VPC 10.0.0.0/16, spanning us-east-1a and us-east-1b

  Public subnets  (route 0.0.0.0/0 -> IGW)
    10.0.1.0/24  us-east-1a   -> load balancer + NAT gateway
    10.0.2.0/24  us-east-1b   -> load balancer + NAT gateway

  Private subnets (route 0.0.0.0/0 -> NAT in the same AZ)
    10.0.11.0/24 us-east-1a   -> app servers, database
    10.0.12.0/24 us-east-1b   -> app servers, database

The load balancer sits in the public subnets - it is the only thing with a route to the internet gateway and a security group open to the world on 443. Behind it, the app servers and database live in the private subnets, unreachable from the internet, accepting traffic only from the load balancer's and app tier's security groups. Each private subnet routes outbound through a NAT gateway in its own AZ, so instances can still pull updates. Everything is duplicated across two AZs, so if us-east-1a fails, the load balancer keeps serving from the us-east-1b instances.

This is the shape to keep in your head: public subnet for the internet-facing load balancer, private subnets for app and data, spread across AZs. Nearly every real architecture is a variation on it. (For how the instances themselves are launched, sized, and connected to, see the EC2 guide.)

VPC endpoints: reaching AWS services privately

There is one more path worth knowing, because it saves both money and exposure. When a private instance talks to an AWS service like S3 or DynamoDB, the default path is out through the NAT gateway, across the public internet backbone, and back - which costs NAT data-processing charges and sends the traffic outside your VPC.

A VPC endpoint gives you a private path to AWS services without ever leaving the AWS network - no internet gateway, no NAT gateway, no public IPs. There are two kinds:

  • Gateway endpoints - for S3 and DynamoDB only. You add a route to your route table pointing S3/DynamoDB traffic at the endpoint. They are free and are the fix for the "our NAT bill is huge because we read a lot from S3" problem - traffic to S3 now bypasses the NAT gateway entirely.
  • Interface endpoints (powered by AWS PrivateLink) - for most other services (SQS, SNS, Secrets Manager, ECR, and many more). These create an elastic network interface with a private IP inside your subnet, and you reach the service through it. They cost a small hourly rate plus per-GB, but keep traffic private and off the internet.

The everyday win is the S3 gateway endpoint: it is free, quick to add, and cuts NAT costs and internet exposure for any workload that talks to S3 a lot. Reach for it early.

Walking a packet from the internet to a private instance

To lock the whole model in, trace one real request - a user hitting your app over HTTPS - all the way in and back out. Every hop is one of the pieces above doing its job.

  1. The user's request arrives at your load balancer's public IP over TCP 443. It enters the VPC through the internet gateway, which is the only reason a public address is reachable at all.
  2. The public subnet's NACL checks the traffic at the subnet boundary. Default NACLs allow it; a custom one must permit inbound 443 (and, being stateless, outbound on ephemeral ports for the reply).
  3. The load balancer's security group is checked at the instance level. It allows inbound 443 from 0.0.0.0/0, so the connection is accepted. The security group is stateful, so it will let the response back out automatically.
  4. The load balancer forwards the request to an app server in a private subnet. This hop never touches the internet - it is local VPC routing between subnets, both inside 10.0.0.0/16.
  5. The app server's security group allows inbound (say TCP 8080) only from the load balancer's security group. Traffic from anywhere else would be dropped here. This is the wall that keeps the app tier private even though it is serving real requests.
  6. The app processes the request and, if it needs to call out - fetch a secret, read from S3, hit a third-party API - its subnet's route table sends that outbound traffic either to a VPC endpoint (for S3/DynamoDB, staying on AWS's network) or to the NAT gateway (for the general internet). The NAT translates the private source to a public address and sends it out through the IGW; nothing on the internet could have started that conversation.
  7. The response travels back the way it came: app server -> load balancer (allowed by the stateful security group's connection tracking) -> out through the internet gateway -> to the user. No new rules are needed for the return path, because security groups remember the connection.

Every hop is a piece you now know. Routing decided the path (IGW in, NAT/endpoint out, local between subnets), and the two firewalls decided what was allowed (security groups per instance and tier, NACLs per subnet). Nothing else is happening.

The shape of it

A VPC is one thing wearing many hats: your own private network, closed by default, where you open exactly the paths you need. CIDR blocks set the addresses; subnets carve them up across AZs. A subnet is public only because its route table points 0.0.0.0/0 at an internet gateway - there is no magic checkbox. Private subnets reach out through a NAT gateway (which costs real money per hour and per GB, so mind it), or better yet through a free VPC endpoint for S3 and DynamoDB. Security groups are your everyday, stateful, allow-only firewall at the instance level; network ACLs are a stateless, subnet-wide backstop. Arrange it as a load balancer in public subnets fronting app and data in private subnets, duplicated across AZs, and you have the standard, defensible layout. Trace a packet through it once and the acronyms stop being a wall and become a network you can reason about.