Amazon VPC explained
A beginner-friendly guide to AWS networking: what a VPC is, how traffic moves through one, and how to fix it when it does not.
Checked against the AWS documentation · 20 October 2026
On this page · 20 sections
The short answer
An Amazon VPC (Virtual Private Cloud) is your own private network inside AWS. AWS describes it as a virtual network dedicated to your AWS account and logically isolated from other virtual networks. You choose its range of IP addresses, split it into subnets, and decide with route tables, gateways and security rules what can talk to what, and whether anything can reach the internet.
Almost everything you launch, such as an EC2 instance, a load balancer or an RDS database, lives in a subnet of a VPC. That is why networking is the first thing that goes wrong when an app on AWS “cannot connect”.
An analogy, and where it breaks
Think of the VPC as a private floor you rent in a large office building. You decide how the floor is divided into rooms (subnets). Signs in each room say which corridor leads where (route tables). There is one main entrance to the street (the internet gateway), and a mail room that sends letters out for people in back offices and passes the replies back, but never lets visitors in (the NAT gateway). Each room’s door has a guard with a list of who may enter (security groups), and each corridor has a checkpoint too (network ACLs).
Where the analogy breaks, and why it matters:
- A room is not “public” because of where it is. In AWS a subnet is public only because its route table sends internet traffic to the internet gateway. Change the route and the same subnet becomes private.
- Being in a public room is not enough to be reachable. A resource also needs a public IPv4 address (or an IPv6 address), and its security rules must allow the traffic.
- The guards remember differently. A security group remembers who it let out and lets their replies back in (it is stateful). A network ACL does not: it checks every packet against its list, in both directions (it is stateless).
- A room cannot span floors. Each subnet lives in exactly one Availability Zone, a separate data-centre location. To survive a zone failing, you build matching subnets in at least two zones.
The architecture, part by part
This is the most common shape for a web application on AWS. Choose any part to see what it is for, or choose a request to follow it through the network one step at a time.
VPC 10.0.0.0/16 · one Availability Zone shown; production repeats each subnet in at least two
Public subnet
- What it is for
- Holds the things that must face the internet: load balancer nodes and NAT gateways. Not your application servers.
- How it is set up
- A subnet is public because its route table has a route to an internet gateway. A resource in it is reachable only if it also has a public address and its security rules allow the traffic.
- Security
- Keep it nearly empty. Anything here with a public IP and an open security group is one rule away from exposure.
- Common mistake
- Launching application servers or databases here "to make them work", which gives them public addresses they never needed.
The same diagram as text
- The internet (Users, package mirrors, APIs)
- Internet gateway (The VPC's door to the internet)
- Public subnet (Route table: 0.0.0.0/0 → internet gateway), containing: Application Load Balancer (Accepts HTTPS from the internet); NAT gateway (Outbound only, for private subnets)
- Private app subnet (Route table: 0.0.0.0/0 → NAT gateway), containing: Application servers (EC2 instances, private IPs only); S3 gateway endpoint (Private path to S3, no NAT)
- Private database subnet (Route table: local route only), containing: Database (Amazon RDS) (Not publicly accessible)
The three subnets sit inside the VPC (10.0.0.0/16). The internet gateway is attached to the VPC. The NAT gateway is a separate outbound path: private servers send internet-bound traffic to it, and it forwards that traffic through the internet gateway.
The VPC and its address range
When you create a VPC you give it an IPv4 range in CIDR notation, such as 10.0.0.0/16. CIDR (Classless Inter-Domain Routing) writes a range as a starting address and how many leading bits are fixed: /16 fixes 16 bits and leaves 65,536 addresses. AWS allows a VPC between /16 (65,536 addresses) and /28 (16), and recommends the private ranges from RFC 1918: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16.
Pick a range that will not overlap networks you may need to connect later, such as other VPCs or your office. Peering, for example, refuses overlapping ranges. The CIDR calculator shows what a range contains and how it splits into subnets.
Subnets, public and private
A subnet is a smaller range inside the VPC, such as 10.0.10.0/24, and it lives entirely in one Availability Zone. Subnets can be between /28 and /16, and AWS keeps five addresses in each: in 10.0.0.0/24 those are .0 (network), .1 (VPC router), .2 (DNS), .3 (reserved) and .255 (broadcast, not supported in a VPC). A /24 therefore gives you 251 usable addresses, not 256.
AWS defines the subnet types by routing:
- Public subnet: has a direct route to an internet gateway.
- Private subnet: has no direct route to an internet gateway; it needs a NAT device to reach the internet.
- Isolated subnet: has no route out of the VPC at all. The database subnet in the diagram is one.
“Public” describes routing, not exposure. Whether a resource can actually be reached also depends on it having a public address, and on its security group and network ACL allowing the traffic. Put only what must face the internet, such as load balancers and NAT gateways, in public subnets.
Route tables, and the local route
A route table is a list of rules: each one has a destination (an address range) and a target (where to send matching traffic). Every subnet is associated with exactly one route table; a subnet you do not associate explicitly uses the VPC’s main route table.
Every route table contains a local route for the VPC’s own range, for example 10.0.0.0/16 → local. That is why every subnet in a VPC can reach every other subnet without you adding anything. When several routes match, AWS uses the most specific one (longest prefix match).
The three route tables in the diagram
Public subnet Private app subnet Private DB subnet
10.0.0.0/16 local 10.0.0.0/16 local 10.0.0.0/16 local
0.0.0.0/0 igw-... 0.0.0.0/0 nat-...
pl-... (S3) vpce-...AWS suggests leaving the main route table in its original state, with only the local route, and associating each subnet with a custom table explicitly. Then a new subnet you forget to associate cannot accidentally become public.
Internet gateway and NAT gateway
The internet gateway connects the VPC to the internet. AWS describes it as horizontally scaled, redundant and highly available, so it is not a bottleneck. It does no filtering: it lets resources with a public IPv4 address (or IPv6) reach the internet and be reached from it, and security groups and network ACLs decide what is allowed.
A NAT gateway (network address translation) lets instances in private subnets start connections to the internet, for package updates or external APIs, while unsolicited connections from the internet cannot reach them. It swaps the private source address for its own public one and passes the replies back. It is not a firewall for outbound traffic: your security groups and network ACLs still decide what may leave.
AWS now offers two availability modes:
- Zonal (the classic design): you create the NAT gateway in a public subnet with an Elastic IP. Each one lives in one Availability Zone, so AWS recommends one per zone: if a shared NAT gateway’s zone fails, resources in the other zones lose internet access.
- Regional: one NAT gateway that expands into the zones where you have workloads, with no public subnet needed. AWS notes it can take up to 60 minutes to expand into a new zone, and that it does not do private NAT; for that, use zonal mode.
NAT gateways cost money even when idle
You pay for every hour a NAT gateway exists, plus every gigabyte it processes. In AWS’s own example for US East (Ohio), both are $0.045: per hour, and per GB. A regional NAT gateway is charged per hour in each zone it covers. Public IPv4 addresses, including the NAT gateway’s Elastic IP, cost $0.005 per hour whether in use or idle. Prices vary by Region; check the pricing page for yours. Traffic to S3 or DynamoDB can skip the NAT gateway entirely through a free gateway endpoint (below).
Security groups and network ACLs
Both filter traffic, at different levels. AWS’s advice is to use security groups as the main control and network ACLs as coarse, subnet-wide guard rails.
| Security group | Network ACL | |
|---|---|---|
| Applies to | A network interface (an instance, a load balancer) | A whole subnet |
| State | Stateful: replies are allowed automatically | Stateless: replies need their own rule |
| Rules | Allow only; no deny rules | Allow and deny |
| Evaluation | All rules together | In order, lowest number first; the first match decides |
| Can reference | Other security groups, such as “the load balancer’s group” | Address ranges only |
A new security group has no inbound rules, so nothing gets in until you add some, and one outbound rule allowing everything. Referencing security groups instead of IP ranges is what keeps the diagram’s chain tight: the app servers accept only the load balancer, and the database accepts only the app servers.
The load balancer and the private database
An internet-facing Application Load Balancer receives HTTPS from users and forwards it to healthy app servers. AWS requires it to be in subnets in at least two Availability Zones, one subnet per zone. The app servers stay in private subnets: only the load balancer needs to be public.
An RDS database belongs in its own private subnets. RDS needs a DB subnet group with subnets in at least two Availability Zones in the Region, even for a single-instance database, so it can be moved or made Multi-AZ later. Leave “publicly accessible” off, and allow the database port only from the app servers’ security group.
VPC endpoints, peering, Transit Gateway and Flow Logs
- Gateway endpoints give private subnets a route to Amazon S3 or DynamoDB in the same Region without an internet gateway or NAT device, at no additional charge. Other AWS services use interface endpoints (AWS PrivateLink), which are billed per hour and per GB processed.
- VPC peering connects two VPCs privately, one to one. Their address ranges may not overlap, and peering is not transitive: if A peers with B and B with C, A still cannot reach C through B.
- Transit Gateway is a hub that many VPCs and on-premises networks attach to, instead of a mesh of peering connections.
- VPC Flow Logs record the IP traffic going to and from network interfaces: who, where, which port, and whether it was accepted or rejected. AWS collects them outside the path of your traffic, so turning them on does not affect throughput or latency.
Four requests, step by step
The same walkthroughs as the diagram above, for reading straight through.
A user opens the app
- The internet: The browser resolves your domain to the load balancer and sends an HTTPS request to it.
- Internet gateway: The request enters the VPC through the internet gateway, the only way in from the internet.
- Application Load Balancer: The load balancer's security group allows 443 from anywhere, so it accepts the request and picks a healthy app server.
- Application servers: The app server's security group allows the app port only from the load balancer's group, so this request is allowed. The VPC's local route carries it between subnets.
- Application Load Balancer: The response goes back the same way. Security groups are stateful, so the reply is allowed without a separate rule.
The app queries the database
- Application servers: The app server connects to the database's private address. No gateway is involved.
- Database (Amazon RDS): The local route (for example 10.0.0.0/16 → local) delivers it. The database's security group allows its port only from the app servers' group, so the connection is accepted.
A private server downloads updates
- Application servers: The package manager connects to a mirror on the internet. The app subnet's route table sends 0.0.0.0/0 to the NAT gateway.
- NAT gateway: The NAT gateway replaces the private source address with its own public Elastic IP and keeps track of the connection.
- Internet gateway: The NAT gateway's subnet is public, so its route table sends the traffic to the internet gateway.
- The internet: The mirror replies to the NAT gateway's address. The NAT gateway passes the reply back to the server. A connection started from the internet has no such entry, so it never reaches the server.
The app reads from S3
- Application servers: The app requests an object from S3. Its route table has a route to the S3 prefix list through the gateway endpoint, and AWS uses the most specific matching route, so this one wins over 0.0.0.0/0. Only for S3 in the same Region: prefix lists are per Region.
- S3 gateway endpoint: Traffic goes to S3 through the endpoint, not the NAT gateway, so there is no NAT data processing charge for it.
What breaks when one setting changes
The diagram’s “What if…” switches show this interactively. Each change below breaks one request, or makes it cost more, and leaves the others alone, which is how you can tell from the symptom which setting moved.
- Remove the public subnet's route to the internet gateway. A user opens the app: stops at the application load balancer. An internet-facing load balancer must sit in public subnets. Without the route, requests from the internet never get an answer. A private server downloads updates: stops at the nat gateway. The NAT gateway's own subnet can no longer reach the internet gateway, so nothing behind it can reach the internet.
- Remove the app subnet's 0.0.0.0/0 route to the NAT gateway. A private server downloads updates: stops at the application servers. No route matches internet addresses any more, so the download times out. Traffic inside the VPC still uses the local route.
- Delete the S3 gateway endpoint. The app reads from S3: still works, but costs more. S3 traffic now follows 0.0.0.0/0 through the NAT gateway. It still works, but every gigabyte is billed as NAT data processing.
- App servers' security group stops allowing the load balancer. A user opens the app: stops at the application servers. The load balancer cannot reach any target and health checks fail, so users get errors from the load balancer instead of the app.
- Database security group allows its port only from 10.0.20.0/24 (its own subnet). The app queries the database: stops at the database (amazon rds). The app servers are in 10.0.10.0/24, which no rule allows. Security groups have no deny rules: anything not allowed is dropped.
Build it with the AWS CLI
Read before running. These commands follow AWS’s own CLI tutorial for this architecture, and every option was checked against the AWS CLI 2.32 help. They were not run against an AWS account for this page. A NAT gateway and an Elastic IP are billed by the hour from the moment they exist, so run the clean-up at the end when you are done. Use a test account, never production.
You need the AWS CLI v2 configured for a test account, and permission to create VPC resources. Every create command prints an ID; use yours where these examples show vpc-0abc, subnet-0pub and so on.
1–2. The network and its public subnet
# Region and AZ used throughout; change both to yours. # IDs below (vpc-0abc..., subnet-0pub...) are placeholders: use what each command prints. # 1. The VPC and three subnets in one Availability Zone aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query Vpc.VpcId --output text aws ec2 create-subnet --vpc-id vpc-0abc --cidr-block 10.0.0.0/24 --availability-zone us-east-2a # public aws ec2 create-subnet --vpc-id vpc-0abc --cidr-block 10.0.10.0/24 --availability-zone us-east-2a # app aws ec2 create-subnet --vpc-id vpc-0abc --cidr-block 10.0.20.0/24 --availability-zone us-east-2a # db # 2. Internet gateway, and the route that makes the first subnet public aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text aws ec2 attach-internet-gateway --internet-gateway-id igw-0abc --vpc-id vpc-0abc aws ec2 create-route-table --vpc-id vpc-0abc --query RouteTable.RouteTableId --output text aws ec2 create-route --route-table-id rtb-0pub --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0abc aws ec2 associate-route-table --route-table-id rtb-0pub --subnet-id subnet-0pub
3–5. Outbound for private subnets, and S3 without NAT
# 3. NAT gateway in the public subnet. The hourly charge starts here. aws ec2 allocate-address --domain vpc --query AllocationId --output text aws ec2 create-nat-gateway --subnet-id subnet-0pub --allocation-id eipalloc-0abc --query NatGateway.NatGatewayId --output text aws ec2 wait nat-gateway-available --nat-gateway-ids nat-0abc # 4. The app subnet's route table: 0.0.0.0/0 goes to the NAT gateway aws ec2 create-route-table --vpc-id vpc-0abc --query RouteTable.RouteTableId --output text aws ec2 create-route --route-table-id rtb-0app --destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-0abc aws ec2 associate-route-table --route-table-id rtb-0app --subnet-id subnet-0app # 5. S3 through a gateway endpoint instead of the NAT gateway aws ec2 create-vpc-endpoint --vpc-id vpc-0abc --vpc-endpoint-type Gateway --service-name com.amazonaws.us-east-2.s3 --route-table-ids rtb-0app
Clean up, NAT gateway first
# Delete the NAT gateway first and release its Elastic IP: both are billed by the hour. aws ec2 delete-nat-gateway --nat-gateway-id nat-0abc aws ec2 wait nat-gateway-deleted --nat-gateway-ids nat-0abc aws ec2 release-address --allocation-id eipalloc-0abc # Then the rest, in this order aws ec2 delete-vpc-endpoints --vpc-endpoint-ids vpce-0abc aws ec2 detach-internet-gateway --internet-gateway-id igw-0abc --vpc-id vpc-0abc aws ec2 delete-internet-gateway --internet-gateway-id igw-0abc aws ec2 delete-subnet --subnet-id subnet-0pub # and subnet-0app, subnet-0db aws ec2 delete-route-table --route-table-id rtb-0pub # and rtb-0app aws ec2 delete-vpc --vpc-id vpc-0abc
To check it worked, launch a small instance with no public IP in the app subnet and connect through AWS Systems Manager Session Manager, so you need neither a public address nor SSH.curl -I https://aws.amazon.com should then succeed through the NAT gateway, and a request from the internet to the instance cannot reach it. Session Manager needs the instance to have the right permissions (the AWS managed policy AmazonSSMManagedInstanceCore provides them) and a way to reach the Systems Manager endpoints: the NAT gateway gives it one, or you can add interface endpoints for ssm, ssmmessages and ec2messages.
The same in Terraform
The same single-zone layout as Terraform, for reading alongside the CLI steps. It was formatted and syntax-checked with Terraform 1.14 (terraform fmt) but not validated against the AWS provider or applied; run terraform plan and read it before applying anything.
main.tf
# One Availability Zone shown for brevity; repeat the subnets, NAT gateway
# and private route table per AZ (or use a regional NAT gateway).
variable "region" {
type = string
default = "us-east-2"
}
variable "az" {
type = string
default = "us-east-2a"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "learn-vpc" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.0.0/24"
availability_zone = var.az
tags = { Name = "public-a" }
}
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.10.0/24"
availability_zone = var.az
tags = { Name = "app-a" }
}
resource "aws_subnet" "db" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.20.0/24"
availability_zone = var.az
tags = { Name = "db-a" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
# Public: 0.0.0.0/0 goes to the internet gateway.
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
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# A zonal NAT gateway lives in the public subnet and needs an Elastic IP.
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "a" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
depends_on = [aws_internet_gateway.main]
}
# Private app subnet: 0.0.0.0/0 goes to the NAT gateway, not the internet gateway.
resource "aws_route_table" "app" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.a.id
}
}
resource "aws_route_table_association" "app" {
subnet_id = aws_subnet.app.id
route_table_id = aws_route_table.app.id
}
# Database subnet: only the local route, which every route table has.
resource "aws_route_table" "db" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table_association" "db" {
subnet_id = aws_subnet.db.id
route_table_id = aws_route_table.db.id
}
# S3 without the NAT gateway: a gateway endpoint on the app route table.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.app.id]
}Troubleshooting
Most connectivity problems in a VPC come down to the same questions: does the name resolve, is there a route, is there a public address where one is needed, does the security group allow it, does the network ACL allow it both ways, and is the service on the far end actually listening? The commands below answer them without guessing.
An EC2 instance in a public subnet cannot reach the internet
Being in a "public" subnet is only half of it. The instance also needs a public IPv4 address (or an Elastic IP), and its security group and the subnet's network ACL must allow the traffic both ways.
aws ec2 describe-instances --instance-ids i-0abc --query 'Reservations[].Instances[].PublicIpAddress'
Empty means no public IPv4 address. A subnet you created does not hand them out unless you change its setting or assign one at launch.
aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=subnet-0pub --query 'RouteTables[].Routes[]'
A route with DestinationCidrBlock 0.0.0.0/0 and a GatewayId starting igw-. If the command returns nothing, the subnet uses the VPC's main route table: check that one.
- Associate an Elastic IP, or turn on public addresses for the subnet with aws ec2 modify-subnet-attribute --subnet-id subnet-0pub --map-public-ip-on-launch (affects instances launched afterwards). Note that every public IPv4 address is billed by the hour.
- Add the 0.0.0.0/0 → internet gateway route to the subnet's own route table, and check the network ACL allows both the outbound request and the reply coming back.
The application server cannot connect to RDS
Inside one VPC, routing is rarely the problem: the local route connects every subnet. It is almost always a security group, a network ACL, or connecting to the wrong address.
aws ec2 describe-security-groups --group-ids sg-0db --query 'SecurityGroups[].IpPermissions'
An inbound rule for the database port (5432 for PostgreSQL, 3306 for MySQL) whose source is the app servers' security group.
aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=subnet-0db
Network ACLs are stateless: both the inbound database port and the outbound return traffic must be allowed, in the database subnet and in the app subnet.
- Allow the port from the app servers' security group, not from an IP range, so new app servers are covered automatically.
- Use the RDS endpoint name, not an IP address: the address can change after a failover.
- Still stuck? Run Reachability Analyzer from the instance to the database (below): it names the blocking component.
The public application is unreachable
Work from the outside in: DNS, then the load balancer, then the targets. A load balancer that answers but returns 502 or 503 is a target problem, not a network one.
aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:...
Targets marked unhealthy: the health check cannot reach the app. Usually the app servers' security group does not allow the health-check port from the load balancer's security group.
aws ec2 describe-security-groups --group-ids sg-0alb --query 'SecurityGroups[].IpPermissions'
Inbound 443 (and 80) from 0.0.0.0/0 on the load balancer's group. The load balancer must be internet-facing and in public subnets.
- Allow the app port and the health-check port on the app servers' security group, from the load balancer's security group.
- Check that the load balancer's subnets really route 0.0.0.0/0 to the internet gateway.
A private instance cannot download packages
This is the NAT path from the diagram, and AWS's own troubleshooting list for it is short: the NAT gateway is not available, a route table is wrong, or a security group or network ACL blocks the traffic.
aws ec2 describe-nat-gateways --nat-gateway-ids nat-0abc --query 'NatGateways[].State'
available. A failed NAT gateway is deleted automatically, usually in about an hour; its state message says why it failed.
aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=subnet-0app --query 'RouteTables[].Routes[]'
0.0.0.0/0 with a NatGatewayId. Then the NAT gateway's own subnet must route 0.0.0.0/0 to the internet gateway.
- Point the private subnet's 0.0.0.0/0 at the NAT gateway, and make sure no more specific route sends part of the internet somewhere else.
- Allow outbound HTTPS (443) in the instance's security group, and the outbound request and its return traffic in both subnets' network ACLs.
- If ping is your test, ICMP must be allowed too, and the far end must answer ping at all. Test with curl -I https://… instead.
DNS names do not resolve inside the VPC
Instances use the Amazon DNS server (the Route 53 Resolver) at 169.254.169.253, or the VPC's base address plus two, such as 10.0.0.2. Security groups and network ACLs cannot filter traffic to it, so do not look there first. Look at the VPC's two DNS attributes and its DHCP options.
aws ec2 describe-vpc-attribute --vpc-id vpc-0abc --attribute enableDnsSupport aws ec2 describe-vpc-attribute --vpc-id vpc-0abc --attribute enableDnsHostnames
enableDnsSupport must be true for the Amazon DNS server to answer at all; it is true by default. Private hosted zones in Route 53, and private DNS for interface endpoints, need both attributes true. enableDnsHostnames is false by default except in the default VPC.
dig @169.254.169.253 example.com +short
Run on the instance. An answer here but not without the @ means the instance is using some other DNS server: check the VPC's DHCP options set and the instance's resolver configuration.
- Turn the attribute on: aws ec2 modify-vpc-attribute --vpc-id vpc-0abc --enable-dns-support "{\"Value\":true}" (and the same with --enable-dns-hostnames).
- If you use your own DNS servers through a DHCP options set, they must resolve the names you need, or forward to the Amazon DNS server.
The security group rules look right, but the connection still fails
Security groups are only one of four things on the path: routes, network ACLs, the target's own firewall, and the addresses involved. Look at what the network actually did, rather than at the rules again.
aws ec2 create-network-insights-path --source i-0app --destination i-0db --protocol tcp --destination-port 5432 aws ec2 start-network-insights-analysis --network-insights-path-id nip-0abc aws ec2 describe-network-insights-analyses --network-insights-analysis-ids nia-0abc --query 'NetworkInsightsAnalyses[].[NetworkPathFound,Explanations]'
Reachability Analyzer checks the configuration between the two and, when the path is blocked, names the security group, network ACL, route table or load balancer that blocks it.
aws ec2 create-flow-logs --resource-type Subnet --resource-ids subnet-0app --traffic-type REJECT \ --log-destination-type cloud-watch-logs --log-group-name vpc-rejects \ --deliver-logs-permission-arn arn:aws:iam::123456789012:role/flow-logs-role
REJECT records name the source, destination and port that were dropped. A rejected reply rather than request points at a stateless network ACL.
- Fix what the analysis names. For a network ACL, remember the reply: it returns to the high-numbered port the client picked, so that range must be allowed too.
- If the network allows it, look at the host: an OS firewall, the service listening only on 127.0.0.1, or the wrong port.
Production considerations
- Availability: repeat every subnet in at least two Availability Zones, put the load balancer and the database subnet group across them, and give each zone its own route to a NAT gateway in the same zone (or use a regional NAT gateway).
- Security: only the load balancer is internet-facing. Chain security groups by reference: load balancer → app → database. Use network ACLs sparingly, as subnet-wide guard rails.
- Cost: NAT gateways and public IPv4 addresses are billed by the hour, and NAT data by the gigabyte. Keep S3 and DynamoDB traffic on gateway endpoints, keep instances in the same zone as their NAT gateway, and delete what you do not use.
- Visibility: turn on VPC Flow Logs, at least for rejected traffic, before you need them. They cannot tell you about yesterday if they were off yesterday.
- Address planning: leave room. Choose a VPC range that cannot overlap other VPCs or your office network, and size subnets for growth, remembering the five reserved addresses in each.
Common mistakes
- Calling a subnet private because of its name. Only its route table decides.
- Putting app servers in public subnets “so they can download updates”. That is what the NAT gateway is for.
- Thinking a NAT gateway makes a workload secure. It stops unsolicited inbound connections; it does nothing about what your instances send out, or about traffic arriving through the load balancer.
- Opening a database’s security group to
0.0.0.0/0while debugging, then forgetting it. - Allowing a request through a network ACL but not its reply, which comes back on a high-numbered port the client chose.
- Leaving a NAT gateway and its Elastic IP running after a lab.
Interview questions
What makes a subnet public?
Its route table has a route to an internet gateway. A resource in it still needs a public IPv4 or IPv6 address, and security rules that allow the traffic, to be reachable.
Why put the NAT gateway in a public subnet?
A zonal NAT gateway sends traffic to the internet through the internet gateway, so its own subnet must route 0.0.0.0/0 to the internet gateway. A regional NAT gateway is the exception: it needs no public subnet.
Security group or network ACL: which would you use to block one IP address?
A network ACL, because security groups have no deny rules. For allowing traffic, security groups are the main tool: they are stateful and can reference other security groups.
Two VPCs are peered, but traffic does not flow. What do you check?
Routes in both VPCs pointing at the peering connection, security groups on both sides, and that the ranges do not overlap. Also that traffic is not expected to pass through a third VPC: peering is not transitive.
How would you cut a large NAT gateway bill?
Find what the traffic is. Move S3 and DynamoDB traffic to gateway endpoints, keep instances and their NAT gateway in the same Availability Zone to avoid cross-zone charges, and remove NAT gateways nothing uses.
Questions people ask
Do I need a NAT gateway at all?
Only if private instances must start connections to the internet. A database subnet usually needs none, and S3 or DynamoDB access can use a gateway endpoint instead.
What is the default VPC?
An AWS account created after 4 December 2013 has one in each Region, with a public subnet in every Availability Zone, an internet gateway and a route to it. That is why an instance launched there gets internet access straight away. It is handy for experiments; for real workloads, build your own VPC with private subnets.
Can I change a VPC’s address range later?
You can add more IPv4 CIDR blocks to an existing VPC, and later remove those again, but you cannot remove the primary block it was created with. Plan that one before you build.
Where to go next
- Who may use what runs in this network, and why a request is denied: AWS IAM explained.
- The instances themselves, and what survives a stop and start: Amazon EC2 explained.
- Launch an instance into a network like this one, with the choices that matter (keys or Session Manager, IMDSv2, user data): AWS EC2 and VPC: Building Your First Cloud Infrastructure.
- Split your own range into subnets with the CIDR calculator.
- See what the same ideas look like inside a cluster: Kubernetes DNS resolution failures and a Service with no endpoints.
- When the trouble is a certificate rather than the network: x509: certificate signed by unknown authority.
Sources
Every fact on this page comes from these AWS pages, read on 20 October 2026. Prices are AWS's own examples for US East (Ohio) and differ by Region.
- Amazon VPC User Guide: How Amazon VPC works
- Amazon VPC User Guide: Subnet route tables
- Amazon VPC User Guide: Subnets (subnet types)
- Amazon VPC User Guide: Subnet sizing (reserved addresses)
- Amazon VPC User Guide: VPC CIDR blocks
- Amazon VPC User Guide: Internet gateways
- Amazon VPC User Guide: NAT gateway basics
- Amazon VPC User Guide: Regional NAT gateways
- Amazon VPC User Guide: Troubleshoot NAT gateways
- Amazon VPC User Guide: Infrastructure security (security groups and network ACLs)
- Amazon VPC User Guide: Security group rules
- Amazon VPC User Guide: Network ACLs
- Amazon VPC User Guide: VPC Flow Logs
- Amazon VPC User Guide: Understanding Amazon DNS
- Elastic Load Balancing: Troubleshoot your Application Load Balancers
- Amazon VPC User Guide: Create a VPC with private subnets and NAT gateways using the AWS CLI
- AWS PrivateLink: Gateway endpoints
- Amazon VPC Peering Guide: VPC peering basics
- Amazon VPC Transit Gateways: What is AWS Transit Gateway?
- Elastic Load Balancing: Application Load Balancers (subnets)
- Amazon RDS User Guide: Working with a DB instance in a VPC
- Reachability Analyzer: What is Reachability Analyzer?
- Amazon VPC pricing (NAT gateway and public IPv4 examples)
- Amazon VPC FAQs
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it