Build a two-AZ VPC with public and private subnets using the AWS CLI
AWSCore
Tasks
- Create the VPC with DNS hostnames enabled.
- Create the four subnets in the right AZs.
- Create and attach an internet gateway, and a public route table for both public subnets.
- Create a NAT gateway with an Elastic IP in each public subnet.
- Create one private route table per AZ pointing at that AZ's NAT gateway.
- Add an S3 gateway endpoint to both private route tables.
Solution
1VPC and DNS settings
AWS VPC_ID=$(aws ec2 create-vpc --cidr-block 10.10.0.0/16 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=lab-vpc}]' \ --query Vpc.VpcId --output text) aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames '{"Value":true}'DNS support is on by default. DNS hostnames are off by default in a new VPC and private hosted zones need both.
2Four subnets in two AZs
AWS PUB_A=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.10.0.0/24 \ --availability-zone us-east-1a --query Subnet.SubnetId --output text) PUB_B=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.10.1.0/24 \ --availability-zone us-east-1b --query Subnet.SubnetId --output text) PRIV_A=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.10.10.0/24 \ --availability-zone us-east-1a --query Subnet.SubnetId --output text) PRIV_B=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.10.11.0/24 \ --availability-zone us-east-1b --query Subnet.SubnetId --output text)3Internet gateway and public route table
AWS IGW_ID=$(aws ec2 create-internet-gateway \ --query InternetGateway.InternetGatewayId --output text) aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID RT_PUB=$(aws ec2 create-route-table --vpc-id $VPC_ID \ --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_ID aws ec2 associate-route-table --route-table-id $RT_PUB --subnet-id $PUB_A aws ec2 associate-route-table --route-table-id $RT_PUB --subnet-id $PUB_BThe route to the IGW is what makes these subnets public. Leave the main route table without an internet route so a forgotten association fails safe.
4One NAT gateway per AZ, in the public subnets
AWS EIP_A=$(aws ec2 allocate-address --domain vpc --query AllocationId --output text) EIP_B=$(aws ec2 allocate-address --domain vpc --query AllocationId --output text) NAT_A=$(aws ec2 create-nat-gateway --subnet-id $PUB_A --allocation-id $EIP_A \ --query NatGateway.NatGatewayId --output text) NAT_B=$(aws ec2 create-nat-gateway --subnet-id $PUB_B --allocation-id $EIP_B \ --query NatGateway.NatGatewayId --output text) aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_A $NAT_BThe subnet IDs here must be the public ones. A NAT gateway in a private subnet comes up "available" and never forwards anything.
5Private route tables per AZ
AWS RT_PRIV_A=$(aws ec2 create-route-table --vpc-id $VPC_ID \ --query RouteTable.RouteTableId --output text) aws ec2 create-route --route-table-id $RT_PRIV_A \ --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_A aws ec2 associate-route-table --route-table-id $RT_PRIV_A --subnet-id $PRIV_A RT_PRIV_B=$(aws ec2 create-route-table --vpc-id $VPC_ID \ --query RouteTable.RouteTableId --output text) aws ec2 create-route --route-table-id $RT_PRIV_B \ --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_B aws ec2 associate-route-table --route-table-id $RT_PRIV_B --subnet-id $PRIV_BEach AZ uses its own NAT gateway, so losing one AZ does not cut the other off, and no traffic pays the inter-AZ charge.
6S3 gateway endpoint
AWS aws ec2 create-vpc-endpoint --vpc-id $VPC_ID \ --vpc-endpoint-type Gateway \ --service-name com.amazonaws.us-east-1.s3 \ --route-table-ids $RT_PRIV_A $RT_PRIV_BFree, and it keeps S3 traffic off the NAT gateways and their per-GB charge.
Verify
aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC_ID \
--query "Subnets[].[CidrBlock,AvailabilityZone,AvailableIpAddressCount]" --output text
10.10.0.0/24 us-east-1a 250
10.10.1.0/24 us-east-1b 250
10.10.10.0/24 us-east-1a 251
10.10.11.0/24 us-east-1b 251Each /24 has 251 usable addresses after the 5 AWS reserves. The public subnets show 250 because each NAT gateway's interface took one.
aws ec2 describe-route-tables --route-table-ids $RT_PRIV_A \
--query "RouteTables[0].Routes[].[DestinationCidrBlock||DestinationPrefixListId,GatewayId||NatGatewayId,State]" \
--output text
10.10.0.0/16 local active
0.0.0.0/0 nat-0a1b2c3d4e5f60718 active
pl-63a5400a vpce-0b2c3d4e5f6a70819 activeLocal route, the AZ a NAT gateway as default, and the S3 prefix list routed to the gateway endpoint. All active.
APP-A# curl -s -m 5 https://checkip.amazonaws.com
203.0.113.41The instance in private-a reaches the internet and appears as the Elastic IP of the NAT gateway in AZ a.
Mistakes people make
- Creating the NAT gateway in a private subnet. It shows available but has no route to the internet gateway.
- One NAT gateway for both AZs. It is a single point of failure and adds inter-AZ charges.
- Forgetting the subnet association. The subnet silently uses the main route table.
- Leaving auto-assign public IP on for private subnets, which hands out public addresses that cannot be used and are still billed.
- Releasing an Elastic IP that partners have allow-listed.
On other platforms
Azure has no internet gateway object; subnets get a system default route to the internet. Give private subnets a NAT Gateway for outbound.
az group create -n rg-lab -l eastus
az network vnet create -g rg-lab -n vnet-lab --address-prefixes 10.10.0.0/16 \
--subnet-name snet-web --subnet-prefixes 10.10.0.0/24
az network vnet subnet create -g rg-lab --vnet-name vnet-lab -n snet-app \
--address-prefixes 10.10.10.0/24
az network public-ip create -g rg-lab -n pip-nat --sku Standard
az network nat gateway create -g rg-lab -n nat-lab --public-ip-addresses pip-nat
az network vnet subnet update -g rg-lab --vnet-name vnet-lab -n snet-app --nat-gateway nat-labSay it in the interview
"Public and private is decided by route tables. Public subnets route 0.0.0.0/0 to the internet gateway. Each private subnet routes 0.0.0.0/0 to a NAT gateway that sits in the public subnet of the same AZ, so an AZ failure only affects that AZ. I add a free S3 gateway endpoint so bulk S3 traffic skips the NAT gateway charge."