Octet

Build a two-AZ VPC with public and private subnets using the AWS CLI

AWSCore

Tasks

  1. Create the VPC with DNS hostnames enabled.
  2. Create the four subnets in the right AZs.
  3. Create and attach an internet gateway, and a public route table for both public subnets.
  4. Create a NAT gateway with an Elastic IP in each public subnet.
  5. Create one private route table per AZ pointing at that AZ's NAT gateway.
  6. Add an S3 gateway endpoint to both private route tables.

Solution

  1. 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.

  2. 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)
  3. 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_B

    The 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.

  4. 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_B

    The subnet IDs here must be the public ones. A NAT gateway in a private subnet comes up "available" and never forwards anything.

  5. 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_B

    Each AZ uses its own NAT gateway, so losing one AZ does not cut the other off, and no traffic pays the inter-AZ charge.

  6. 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_B

    Free, and it keeps S3 traffic off the NAT gateways and their per-GB charge.

Verify

Output
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    251

Each /24 has 251 usable addresses after the 5 AWS reserves. The public subnets show 250 because each NAT gateway's interface took one.

Output
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     active

Local route, the AZ a NAT gateway as default, and the S3 prefix list routed to the gateway endpoint. All active.

APP-A output
APP-A# curl -s -m 5 https://checkip.amazonaws.com
203.0.113.41

The 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.

Azure
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-lab

Say 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."

Book 3.3 Internet (IP)