A Virtual Private Cloud (VPC) is your own private network inside AWS. Every EC2 instance, load balancer, database and Lambda-in-VPC lives in one. Get the layout right once and everything after it, from security to cost, is easier.
State of AWS on this date. ap-south-1 (Mumbai) has three Availability Zones (a, b, c). Each account gets a default VPC in every region (172.31.0.0/16 with public subnets), good for experiments, but production workloads should use a VPC you design. A NAT gateway is billed per hour plus per GB processed, so it is a common surprise on bills.
A standard two-AZ layout
flowchart TB
NET[Internet] --> IGW[Internet gateway]
subgraph VPC[VPC 10.0.0.0/16]
subgraph AZA[AZ ap-south-1a]
PUB1[Public subnet 10.0.0.0/24<br/>ALB NAT gateway bastion]
PRV1[Private subnet 10.0.10.0/24<br/>app servers]
DB1[Data subnet 10.0.20.0/24<br/>RDS]
end
subgraph AZB[AZ ap-south-1b]
PUB2[Public subnet 10.0.1.0/24]
PRV2[Private subnet 10.0.11.0/24]
DB2[Data subnet 10.0.21.0/24]
end
PUB1 --> NAT1[NAT gateway A]
PUB2 --> NAT2[NAT gateway B]
PRV1 -->|0.0.0.0/0| NAT1
PRV2 -->|0.0.0.0/0| NAT2
PRV1 --> DB1
PRV2 --> DB2
end
IGW --> PUB1
IGW --> PUB2
PRV1 -.->|gateway endpoint| S3[S3 and DynamoDB]
Three tiers, two AZs: the load balancer is public, application servers have no public IPs, and databases accept connections only from the application tier.
Building blocks
| Piece | What it does |
|---|---|
| CIDR block | The address range, for example 10.0.0.0/16 (65,536 addresses). Choose ranges that do not overlap with your offices, other VPCs or partners, because overlap blocks peering and VPN later |
| Subnet | A slice of the VPC inside one AZ. AWS reserves 5 addresses per subnet |
| Route table | Decides where packets for a destination go; each subnet uses one |
| Internet gateway (IGW) | Lets public subnets reach the internet and be reached (needs a public or Elastic IP) |
| NAT gateway | Lets private instances start outbound connections (updates, APIs) without being reachable from outside |
| Security group | Stateful firewall on the instance or network interface; allow rules only |
| Network ACL | Stateless, subnet-level, allow and deny rules; usually left at default |
| VPC endpoint | Private path to AWS services without internet or NAT |
A subnet is “public” only because its route table has 0.0.0.0/0 pointing to an internet gateway.
Create it with the CLI
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query Vpc.VpcId --output text)
aws ec2 modify-vpc-attribute --vpc-id $VPC --enable-dns-hostnames
aws ec2 create-tags --resources $VPC --tags Key=Name,Value=prod-vpc
PUB1=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.0.0/24 --availability-zone ap-south-1a --query Subnet.SubnetId --output text)
PRV1=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.10.0/24 --availability-zone ap-south-1a --query Subnet.SubnetId --output text)
IGW=$(aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC --internet-gateway-id $IGW
RT_PUB=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $RT_PUB --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW
aws ec2 associate-route-table --route-table-id $RT_PUB --subnet-id $PUB1
EIP=$(aws ec2 allocate-address --domain vpc --query AllocationId --output text)
NAT=$(aws ec2 create-nat-gateway --subnet-id $PUB1 --allocation-id $EIP --query NatGateway.NatGatewayId --output text)
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT
RT_PRV=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $RT_PRV --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT
aws ec2 associate-route-table --route-table-id $RT_PRV --subnet-id $PRV1
Treat this as a learning aid; for real environments define the VPC in CloudFormation or Terraform so it is reviewed, repeatable and easy to tear down. Remember to repeat the subnets, route tables and NAT gateway in the second AZ.
Cost and resilience decisions
-
A NAT gateway lives in one AZ. For resilience, one per AZ with each private route table pointing to its own; for a cheap lab, one NAT (or a NAT instance) and accept the risk.
-
Traffic through NAT is billed per GB. Add gateway endpoints for S3 and DynamoDB (free) so heavy S3 traffic bypasses NAT:
aws ec2 create-vpc-endpoint --vpc-id $VPC --service-name com.amazonaws.ap-south-1.s3 --route-table-ids $RT_PRV -
Interface endpoints (PrivateLink) for services such as ECR, SSM and Secrets Manager cost per hour; use them when security or NAT cost justifies it.
-
Cross-AZ traffic costs a little per GB; keep chatty components in the same AZ where you can.
Security and visibility
-
Put only load balancers, bastions (or no bastion, using Session Manager) and NAT in public subnets.
-
Use security groups that reference other security groups (
app-sgallowsalb-sgon 8080), not IP ranges. -
Turn on VPC Flow Logs to CloudWatch Logs or S3 to see accepted and rejected connections when debugging:
aws ec2 create-flow-logs --resource-type VPC --resource-ids $VPC --traffic-type ALL \ --log-destination-type cloud-watch-logs --log-group-name /vpc/prod-flow --deliver-logs-permission-arn arn:aws:iam::111111111111:role/flowlogs -
Leave the default VPC unused (or delete it where policy allows).
Troubleshooting connectivity
Work through: security group (inbound on the target, outbound on the source), network ACL, route table of the source subnet, whether a NAT/IGW exists for the route, DNS settings, and the target’s own listener. The VPC Reachability Analyzer can trace a path between two resources and name the blocking component.