A VPC is a logically isolated virtual network within an AWS region. You define the IP address space (CIDR), subdivide it into subnets, and control traffic with route tables and security constructs.
Key concepts
| Concept | Detail |
|---|---|
| CIDR block | Primary IPv4 CIDR: /16 to /28. Up to 5 secondary CIDRs can be added. IPv6 /56 can be assigned (Amazon-provided or BYOIP). |
| Default VPC | Created automatically per region. Has a /16 CIDR (172.31.0.0/16), public subnets in each AZ, an IGW, and a default route table. Safe to use for testing; recreate with aws ec2 create-default-vpc if deleted. |
| Tenancy | default — instances share hardware with other customers. dedicated — all instances run on single-tenant hardware (significant cost premium). |
| DNS hostnames | Off by default on custom VPCs. Enable enableDnsHostnames to get public DNS names for instances with public IPs. |
| DNS resolution | enableDnsSupport must be on (default) for the Route 53 Resolver at 169.254.169.253 (or VPC base + 2) to work. |
Create a VPC (AWS CLI)
# Create VPC aws ec2 create-vpc \ --cidr-block 10.0.0.0/16 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=my-vpc}]' # Enable DNS hostnames (required for public DNS names) aws ec2 modify-vpc-attribute \ --vpc-id vpc-0abc123 \ --enable-dns-hostnames # Add a secondary CIDR aws ec2 associate-vpc-cidr-block \ --vpc-id vpc-0abc123 \ --cidr-block 10.1.0.0/16 # List VPCs aws ec2 describe-vpcs --query 'Vpcs[*].{ID:VpcId,CIDR:CidrBlock,Default:IsDefault}'
CIDR sizing guide
| Prefix | Addresses | Usable hosts | Typical use |
|---|---|---|---|
| /16 | 65,536 | 65,531 | VPC (max range) |
| /20 | 4,096 | 4,091 | Large subnet |
| /24 | 256 | 251 | Standard subnet |
| /27 | 32 | 27 | Small subnet |
| /28 | 16 | 11 | Minimum VPC/subnet size |
Subnets are subdivisions of a VPC CIDR, each tied to a single Availability Zone. A subnet's "public" or "private" character comes entirely from its route table — whether it has a route to an Internet Gateway.
Public vs private subnet
| Type | Route table has | Typical resources |
|---|---|---|
| Public | 0.0.0.0/0 → IGW | Load balancers, NAT Gateways, bastion hosts |
| Private | 0.0.0.0/0 → NAT Gateway (or none) | App servers, databases, Lambda in VPC |
| Isolated | No default route at all | Databases that should never reach internet |
Create subnets
# Public subnet in AZ a aws ec2 create-subnet \ --vpc-id vpc-0abc123 \ --cidr-block 10.0.0.0/24 \ --availability-zone us-east-1a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]' # Enable auto-assign public IP for public subnets aws ec2 modify-subnet-attribute \ --subnet-id subnet-0abc123 \ --map-public-ip-on-launch # Private subnet in AZ a aws ec2 create-subnet \ --vpc-id vpc-0abc123 \ --cidr-block 10.0.2.0/24 \ --availability-zone us-east-1a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-1a}]' # List subnets with AZ and available IPs aws ec2 describe-subnets \ --filters Name=vpc-id,Values=vpc-0abc123 \ --query 'Subnets[*].{ID:SubnetId,AZ:AvailabilityZone,CIDR:CidrBlock,Available:AvailableIpAddressCount}'
Recommended multi-tier layout
VPC: 10.0.0.0/16 Public subnets (one per AZ — for load balancers, NAT GW): 10.0.0.0/24 us-east-1a 10.0.1.0/24 us-east-1b 10.0.2.0/24 us-east-1c Private subnets (app tier): 10.0.10.0/24 us-east-1a 10.0.11.0/24 us-east-1b 10.0.12.0/24 us-east-1c Isolated subnets (data tier): 10.0.20.0/24 us-east-1a 10.0.21.0/24 us-east-1b 10.0.22.0/24 us-east-1c
Every VPC has a main route table. Subnets not explicitly associated with a custom route table use the main one. Routes are evaluated longest-prefix-first; the local route (VPC CIDR) always wins for intra-VPC traffic and cannot be deleted.
Common routes
| Destination | Target | Purpose |
|---|---|---|
| 10.0.0.0/16 | local | Intra-VPC traffic (auto-added, non-deletable) |
| 0.0.0.0/0 | igw-xxx | Public internet (public subnets) |
| 0.0.0.0/0 | nat-xxx | Outbound internet via NAT (private subnets) |
| 10.1.0.0/16 | pcx-xxx | VPC peering connection |
| 0.0.0.0/0 | tgw-xxx | Transit Gateway (hub-and-spoke) |
| pl-xxx (prefix list) | vpce-xxx | Gateway endpoint (S3, DynamoDB) |
Manage route tables
# Create a route table aws ec2 create-route-table --vpc-id vpc-0abc123 \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=public-rt}]' # Add a route to IGW aws ec2 create-route \ --route-table-id rtb-0abc123 \ --destination-cidr-block 0.0.0.0/0 \ --gateway-id igw-0abc123 # Associate subnet with route table aws ec2 associate-route-table \ --route-table-id rtb-0abc123 \ --subnet-id subnet-0abc123 # View routes aws ec2 describe-route-tables \ --route-table-ids rtb-0abc123 \ --query 'RouteTables[*].Routes'
An Internet Gateway (IGW) enables bidirectional internet connectivity for instances with public IPs. It performs NAT for IPv4 (translating public EIP/auto-assigned IPs to private IPs) and is horizontally scaled and redundant — no bandwidth bottleneck.
How it works
Outbound: EC2 (10.0.0.5) → IGW → Internet IGW translates source 10.0.0.5 → public IP (EIP or auto-assigned) Inbound: Internet → public IP → IGW → EC2 (10.0.0.5) IGW translates destination public IP → 10.0.0.5 Requirements: 1. VPC has an IGW attached 2. Subnet route table has 0.0.0.0/0 → igw-xxx 3. Instance has a public IP (EIP or auto-assign) or is behind a public ELB 4. Security group allows the traffic
Create and attach IGW
# Create aws ec2 create-internet-gateway \ --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=my-igw}]' # Attach to VPC (one IGW per VPC) aws ec2 attach-internet-gateway \ --internet-gateway-id igw-0abc123 \ --vpc-id vpc-0abc123 # Detach aws ec2 detach-internet-gateway \ --internet-gateway-id igw-0abc123 \ --vpc-id vpc-0abc123
A NAT Gateway allows instances in private subnets to initiate outbound internet connections (e.g., to download packages) without being reachable from the internet. It is managed, scales automatically, and is highly available within a single AZ.
NAT Gateway vs NAT Instance
| NAT Gateway | NAT Instance | |
|---|---|---|
| Management | Fully managed by AWS | You manage the EC2 instance |
| Bandwidth | Up to 100 Gbps (scales automatically) | Limited by instance type |
| High availability | HA within one AZ (deploy one per AZ) | Manual — requires scripts/ASG |
| Cost | Hourly + per-GB data charge | EC2 instance cost only |
| Security groups | Not supported | Supported |
| Bastion | Cannot be used as bastion | Can double as bastion |
Create NAT Gateway and route private subnet
# Allocate an Elastic IP aws ec2 allocate-address --domain vpc # Create NAT Gateway in the public subnet aws ec2 create-nat-gateway \ --subnet-id subnet-public-1a \ --allocation-id eipalloc-0abc123 \ --tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=nat-1a}]' # Wait for it to become available aws ec2 wait nat-gateway-available --nat-gateway-ids nat-0abc123 # Add default route in private route table pointing to NAT GW aws ec2 create-route \ --route-table-id rtb-private-1a \ --destination-cidr-block 0.0.0.0/0 \ --nat-gateway-id nat-0abc123
Private NAT Gateway
Security groups are stateful virtual firewalls attached to ENIs (network interfaces). Return traffic is automatically allowed. All inbound is denied by default; all outbound is allowed by default.
Key behaviours
| Property | Detail |
|---|---|
| Stateful | If inbound is allowed, the return traffic is automatically permitted — no outbound rule needed for responses. |
| Allow-only | Rules can only allow traffic; there is no explicit deny. To block traffic, simply have no matching allow rule. |
| Multiple SGs | Up to 5 SGs per ENI. Rules from all attached SGs are evaluated together (union of allows). |
| SG references | Source/destination can be another SG ID, not just a CIDR. This dynamically includes all instances using that SG. |
| Default SG | Allows all inbound from itself (same SG) and all outbound. Never use the default SG for production workloads. |
Manage security groups
# Create aws ec2 create-security-group \ --group-name web-sg \ --description "Web tier" \ --vpc-id vpc-0abc123 # Allow HTTPS from anywhere aws ec2 authorize-security-group-ingress \ --group-id sg-0abc123 \ --protocol tcp --port 443 --cidr 0.0.0.0/0 # Allow app traffic only from the web SG (SG reference) aws ec2 authorize-security-group-ingress \ --group-id sg-app123 \ --protocol tcp --port 8080 \ --source-group sg-0abc123 # Revoke a rule aws ec2 revoke-security-group-ingress \ --group-id sg-0abc123 \ --protocol tcp --port 22 --cidr 0.0.0.0/0 # Show rules aws ec2 describe-security-group-rules \ --filters Name=group-id,Values=sg-0abc123
Recommended layered pattern
sg-alb
Inbound: 443 from 0.0.0.0/0
Outbound: 8080 → sg-app
sg-app
Inbound: 8080 from sg-alb
Outbound: 5432 → sg-db
sg-db
Inbound: 5432 from sg-app
Outbound: (none needed)
sg-bastion
Inbound: 22 from your-ip/32
Outbound: 22 → sg-app
Network ACLs (NACLs) are stateless firewalls applied at the subnet boundary. Both inbound and outbound rules must explicitly allow traffic (including return traffic). Rules are evaluated in ascending rule-number order; the first match wins.
Security Groups vs NACLs
| Security Group | NACL | |
|---|---|---|
| State | Stateful — return traffic auto-allowed | Stateless — must allow both directions |
| Applied at | ENI (instance level) | Subnet boundary |
| Rules | Allow only | Allow and Deny |
| Evaluation | All rules evaluated together | Lowest rule number first, first match wins |
| Default | Deny all inbound, allow all outbound | Default NACL allows all; custom NACLs deny all until rules added |
NACL rule numbering convention
100 Allow HTTPS inbound (443) 110 Allow HTTP inbound (80) 120 Allow ephemeral ports inbound (1024-65535) for return traffic ... 32766 *DENY all (implicit, cannot be removed)
Manage NACLs
# Create NACL aws ec2 create-network-acl --vpc-id vpc-0abc123 # Add inbound allow rule (rule 100, TCP 443) aws ec2 create-network-acl-entry \ --network-acl-id acl-0abc123 \ --rule-number 100 \ --protocol tcp \ --port-range From=443,To=443 \ --cidr-block 0.0.0.0/0 \ --rule-action allow \ --ingress # Allow ephemeral return ports inbound (stateless — required!) aws ec2 create-network-acl-entry \ --network-acl-id acl-0abc123 \ --rule-number 120 \ --protocol tcp \ --port-range From=1024,To=65535 \ --cidr-block 0.0.0.0/0 \ --rule-action allow \ --ingress # Associate NACL with subnet aws ec2 replace-network-acl-association \ --association-id aclassoc-0abc123 \ --network-acl-id acl-0abc123
VPC peering creates a private network connection between two VPCs — same or different accounts, same or different regions. Traffic stays on the AWS backbone. There are no bandwidth bottlenecks or single points of failure.
Constraints
| Constraint | Detail |
|---|---|
| No transitive routing | If VPC-A peers with VPC-B and VPC-B peers with VPC-C, VPC-A cannot reach VPC-C via VPC-B. Each pair needs its own peering connection. |
| No overlapping CIDRs | Peered VPCs cannot have overlapping CIDR blocks. |
| Route table update required | Peering alone doesn't route traffic — you must add routes in both VPCs pointing the remote CIDR at the peering connection. |
| DNS resolution | Enable "Allow DNS resolution from remote VPC" on both sides to resolve private DNS across the peering. |
Set up VPC peering
# Step 1: Create peering request (from VPC A) aws ec2 create-vpc-peering-connection \ --vpc-id vpc-aaa \ --peer-vpc-id vpc-bbb \ --peer-region us-west-2 # omit for same-region # Step 2: Accept (from VPC B's account/region) aws ec2 accept-vpc-peering-connection \ --vpc-peering-connection-id pcx-0abc123 # Step 3: Add routes in VPC A's route table aws ec2 create-route \ --route-table-id rtb-aaa \ --destination-cidr-block 10.1.0.0/16 \ --vpc-peering-connection-id pcx-0abc123 # Step 4: Add routes in VPC B's route table aws ec2 create-route \ --route-table-id rtb-bbb \ --destination-cidr-block 10.0.0.0/16 \ --vpc-peering-connection-id pcx-0abc123 # Step 5: Update security groups to allow traffic from peer CIDR
n(n-1)/2 connections. Prefer Transit Gateway at scale.Transit Gateway (TGW) is a regional network hub that connects VPCs, VPNs, and Direct Connect in a hub-and-spoke model. Transitive routing is supported — any attachment can reach any other via the TGW's route tables.
Architecture
VPC-A ──┐ VPC-B ──┤ VPC-C ──┼──► Transit Gateway ──► VPN (on-premises) VPC-D ──┤ └──► Direct Connect VPC-E ──┘ Each VPC attaches via a TGW VPC attachment (one subnet per AZ). TGW route tables control which attachments can talk to each other.
TGW route table isolation example
TGW Route Table: "shared-services-rt" Associates: VPC-shared (DNS, AD, monitoring) Propagates from: all VPCs → All VPCs can reach shared services TGW Route Table: "spoke-rt" Associates: VPC-A, VPC-B, VPC-C (app VPCs) Propagates from: VPC-shared only → App VPCs can reach shared services but NOT each other
Create and attach TGW
# Create TGW aws ec2 create-transit-gateway \ --description "Main TGW" \ --options DefaultRouteTableAssociation=enable,DefaultRouteTablePropagation=enable # Attach a VPC (one subnet per AZ recommended) aws ec2 create-transit-gateway-vpc-attachment \ --transit-gateway-id tgw-0abc123 \ --vpc-id vpc-0abc123 \ --subnet-ids subnet-1a subnet-1b # Route VPC traffic to TGW aws ec2 create-route \ --route-table-id rtb-0abc123 \ --destination-cidr-block 10.0.0.0/8 \ --transit-gateway-id tgw-0abc123 # Share TGW to other accounts via RAM aws ram create-resource-share \ --name tgw-share \ --resource-arns arn:aws:ec2:us-east-1:123456789:transit-gateway/tgw-0abc123 \ --principals 987654321098
VPC Endpoints allow private connectivity to AWS services without traversing the internet, NAT, or an IGW. Traffic stays entirely within the AWS network.
Endpoint types
| Type | Services | Mechanism | Cost |
|---|---|---|---|
| Gateway endpoint | S3, DynamoDB only | Route table entry pointing a managed prefix list at the endpoint. No ENI created. | Free |
| Interface endpoint | Most AWS services (EC2 API, STS, SQS, SNS, …) | Creates an ENI with a private IP in your subnet. DNS resolves service hostname to that IP. | Hourly + per-GB |
| Gateway Load Balancer endpoint | Third-party appliances (firewalls, IDS) | Routes traffic through a GWLB for inspection before forwarding. | Hourly + per-GB |
Gateway endpoint for S3
# Create gateway endpoint (free, add to route tables) aws ec2 create-vpc-endpoint \ --vpc-id vpc-0abc123 \ --service-name com.amazonaws.us-east-1.s3 \ --vpc-endpoint-type Gateway \ --route-table-ids rtb-private-1a rtb-private-1b # The route table gets a new entry automatically: # Destination: pl-xxxxxx (S3 prefix list) → vpce-xxxxxx # Restrict S3 bucket access to VPC only (bucket policy) # Add this condition to your S3 bucket policy: "Condition": { "StringNotEquals": { "aws:SourceVpce": "vpce-0abc123" } }
Interface endpoint
# Create interface endpoint for SSM (enables Session Manager without internet) for svc in ssm ssmmessages ec2messages; do aws ec2 create-vpc-endpoint \ --vpc-id vpc-0abc123 \ --service-name com.amazonaws.us-east-1.$svc \ --vpc-endpoint-type Interface \ --subnet-ids subnet-private-1a subnet-private-1b \ --security-group-ids sg-endpoints \ --private-dns-enabled done # With --private-dns-enabled, the service's public DNS name # (e.g. ssm.us-east-1.amazonaws.com) resolves to the ENI private IP # — no code changes needed in the application.
VPC Flow Logs capture metadata about IP traffic flowing through VPCs, subnets, or individual ENIs. They are the primary tool for network troubleshooting, security auditing, and anomaly detection in AWS.
Flow log fields (default format)
version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status Example accepted record: 2 123456789 eni-0abc123 10.0.1.5 10.0.2.8 54321 443 6 10 4096 1620000000 1620000060 ACCEPT OK Example rejected record: 2 123456789 eni-0abc123 1.2.3.4 10.0.1.5 54321 22 6 1 40 1620000000 1620000060 REJECT OK
Create flow logs
# Flow logs to CloudWatch Logs aws ec2 create-flow-logs \ --resource-type VPC \ --resource-ids vpc-0abc123 \ --traffic-type ALL \ --log-destination-type cloud-watch-logs \ --log-group-name /aws/vpc/flowlogs \ --deliver-logs-permission-arn arn:aws:iam::123456789:role/flowlogs-role # Flow logs to S3 (cheaper for long-term retention) aws ec2 create-flow-logs \ --resource-type VPC \ --resource-ids vpc-0abc123 \ --traffic-type ALL \ --log-destination-type s3 \ --log-destination arn:aws:s3:::my-flowlogs-bucket/vpc-logs/ # Custom format — include more fields aws ec2 create-flow-logs \ --resource-type VPC \ --resource-ids vpc-0abc123 \ --traffic-type ALL \ --log-format '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${action} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr}'
Query flow logs with CloudWatch Insights
# Top talkers by bytes fields srcaddr, dstaddr, bytes | stats sum(bytes) as total by srcaddr, dstaddr | sort total desc | limit 20 # Find rejected traffic (security group blocks) fields srcaddr, dstaddr, dstport, action | filter action = "REJECT" | stats count() as rejects by srcaddr, dstport | sort rejects desc # Traffic to a specific port fields srcaddr, dstaddr, dstport, action | filter dstport = 22 and action = "REJECT"
Every VPC has a built-in DNS resolver at the VPC base address + 2 (e.g., 10.0.0.2 for a 10.0.0.0/16 VPC). Route 53 Resolver extends this with inbound/outbound endpoints for hybrid DNS resolution with on-premises networks.
DNS resolution in a VPC
| Query type | Resolution |
|---|---|
| AWS service hostnames (e.g. s3.amazonaws.com) | Resolved by Route 53 Resolver to public or private IPs depending on VPC endpoint configuration. |
| Private hosted zones | Resolved by Route 53 Resolver if the zone is associated with the VPC. |
| EC2 internal hostnames | ip-10-0-1-5.us-east-1.compute.internal — resolved within the VPC automatically. |
| External hostnames | Forwarded by the Resolver to public DNS (Route 53 recursive resolver). |
Route 53 Resolver endpoints (hybrid DNS)
Inbound endpoint — on-premises DNS → Resolver endpoint ENIs in VPC
on-prem servers can query AWS private hosted zones
Outbound endpoint — Route 53 Resolver → Resolver endpoint → on-premises DNS
VPC instances can resolve on-premises domain names
using Resolver forwarding rules
# Create outbound endpoint (for forwarding to on-premises) aws route53resolver create-resolver-endpoint \ --creator-request-id my-endpoint \ --name outbound-to-onprem \ --security-group-ids sg-0abc123 \ --direction OUTBOUND \ --ip-addresses SubnetId=subnet-1a SubnetId=subnet-1b # Create forwarding rule: corp.example.com → on-premises DNS aws route53resolver create-resolver-rule \ --creator-request-id my-rule \ --rule-type FORWARD \ --domain-name corp.example.com \ --resolver-endpoint-id rslvr-out-0abc123 \ --target-ips Ip=192.168.1.53,Port=53 # Associate rule with VPC aws route53resolver associate-resolver-rule \ --resolver-rule-id rslvr-rr-0abc123 \ --vpc-id vpc-0abc123
DHCP option sets
# Create custom DHCP options (e.g., custom domain search) aws ec2 create-dhcp-options \ --dhcp-configurations \ "Key=domain-name,Values=corp.example.com" \ "Key=domain-name-servers,Values=AmazonProvidedDNS" # Associate with VPC aws ec2 associate-dhcp-options \ --dhcp-options-id dopt-0abc123 \ --vpc-id vpc-0abc123
Common AWS CLI one-liners
# List all VPCs aws ec2 describe-vpcs --query 'Vpcs[*].{ID:VpcId,CIDR:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value}' # Find which SG is blocking a port aws ec2 describe-security-group-rules \ --filters Name=group-id,Values=sg-0abc123 \ --query 'SecurityGroupRules[?ToPort==`443`]' # Find instances in a subnet aws ec2 describe-instances \ --filters Name=subnet-id,Values=subnet-0abc123 \ --query 'Reservations[*].Instances[*].{ID:InstanceId,IP:PrivateIpAddress,State:State.Name}' # Check NAT Gateway data usage aws cloudwatch get-metric-statistics \ --namespace AWS/NATGateway \ --metric-name BytesOutToDestination \ --dimensions Name=NatGatewayId,Value=nat-0abc123 \ --start-time 2026-05-12T00:00:00Z \ --end-time 2026-05-13T00:00:00Z \ --period 86400 --statistics Sum # Trace route table for a subnet aws ec2 describe-route-tables \ --filters Name=association.subnet-id,Values=subnet-0abc123 \ --query 'RouteTables[*].Routes'
VPC / Subnets
VPC CIDR: /16 to /28
AWS reserves 5 IPs per subnet
Public = route to IGW
Default VPC: 172.31.0.0/16
Security Groups
Stateful — no return rules needed
Allow-only (no deny rules)
Reference SG IDs, not just CIDRs
Max 5 SGs per ENI
NACLs
Stateless — allow both directions
First matching rule number wins
Always allow ephemeral 1024–65535
Custom NACL starts with DENY ALL
NAT Gateway
Deploy one per AZ
Lives in public subnet
Requires an EIP
No security groups; use route tables
VPC Peering
No transitive routing
No overlapping CIDRs
Update route tables on both sides
n(n-1)/2 connections at full mesh
VPC Endpoints
Gateway: S3 + DynamoDB (free)
Interface: most services (hourly fee)
--private-dns-enabled for transparent use
Restrict via endpoint policies
Flow Logs
VPC / Subnet / ENI level
ACCEPT or REJECT per flow
S3 cheaper; CWL for live queries
Does not capture DNS or DHCP
Transit Gateway
Hub-and-spoke replaces full mesh
Supports transitive routing
Share via RAM cross-account
Inter-region peering available