Files
kodekloud-engineer/aws-45.md

9.9 KiB
Raw Permalink Blame History

Task 45

The Nautilus DevOps team is tasked with enabling internet access for an EC2 instance running in a private subnet. This instance should be able to upload a test file to a public S3 bucket once it can access the internet. To achieve this, the team must set up a NAT Gateway in a public subnet within the same VPC.

  1. A VPC named xfusion-priv-vpc and a private subnet xfusion-priv-subnet have already been created.
  2. An EC2 instance named xfusion-priv-ec2 is already running in the private subnet.
  3. The EC2 instance is configured with a cron job that uploads a test file to a bucket xfusion-nat-285654546 once internet is accessible.

Your task is to:

Create a public subnet named xfusion-pub-subnet in the same VPC. Create an Internet Gateway and attach it to the VPC. Create a route table xfusion-pub-rt and associate it with the public subnet. Allocate an Elastic IP and create a NAT Gateway named xfusion-natgw. Update the private route table to route 0.0.0.0/0 traffic via the NAT Gateway. Once complete, verify that the EC2 instance can reach the internet by confirming the presence of the test file in the S3 bucket xfusion-nat-285654546. After completing all the configuration, please wait a few minutes for the test file to appear in the bucket, as it may take 23 minutes.

Solution

NAT Gateway Task (xfusion-priv-vpc)

The managed NAT Gateway version — contrast with the earlier NAT instance task, which needed a self-managed EC2 box with source/dest-check disabled, IP forwarding, and iptables MASQUERADE. A NAT Gateway is a fully AWS-managed resource: no instance, no OS config, no source/dest check, auto-scaling and HA within its AZ. You just place it in a public subnet, give it an EIP, and route the private subnet's default route at it. Much less to get wrong.

The architecture: private subnet → its route table 0.0.0.0/0 → NAT GW → NAT GW (in public subnet) → public subnet's route table 0.0.0.0/0 → IGW → internet → S3. Two route tables, two different default routes.

Run on aws-client.

Phase 1 — Discover VPC + private subnet

REGION=us-east-1

VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=xfusion-priv-vpc \
  --region $REGION --query 'Vpcs[0].VpcId' --output text)
VPC_CIDR=$(aws ec2 describe-vpcs --vpc-ids $VPC_ID --region $REGION \
  --query 'Vpcs[0].CidrBlock' --output text)

PRIV_SUBNET=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=xfusion-priv-subnet" \
  --region $REGION --query 'Subnets[0].SubnetId' --output text)
PRIV_AZ=$(aws ec2 describe-subnets --subnet-ids $PRIV_SUBNET --region $REGION \
  --query 'Subnets[0].AvailabilityZone' --output text)

Phase 2 — Public subnet + Internet Gateway

# Pick a free /24 in the VPC for the public subnet
BASE=$(echo $VPC_CIDR | cut -d. -f1-2)
USED=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" \
  --region $REGION --query 'Subnets[].CidrBlock' --output text | tr '\t' '\n')
for i in $(seq 1 254); do
  CAND="${BASE}.${i}.0/24"
  echo "$USED" | grep -Fxq "$CAND" || { PUB_CIDR=$CAND; break; }
done

# Public subnet (same AZ as private keeps NAT traffic in-AZ, avoids cross-AZ charges)
PUB_SUBNET=$(aws ec2 create-subnet \
  --vpc-id $VPC_ID --cidr-block $PUB_CIDR \
  --availability-zone $PRIV_AZ --region $REGION \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=xfusion-pub-subnet}]' \
  --query 'Subnet.SubnetId' --output text)

aws ec2 modify-subnet-attribute --subnet-id $PUB_SUBNET \
  --map-public-ip-on-launch --region $REGION

# Internet Gateway (reuse if the VPC already has one)
IGW_ID=$(aws ec2 describe-internet-gateways \
  --filters "Name=attachment.vpc-id,Values=$VPC_ID" \
  --region $REGION --query 'InternetGateways[0].InternetGatewayId' --output text)
if [ "$IGW_ID" = "None" ] || [ -z "$IGW_ID" ]; then
  IGW_ID=$(aws ec2 create-internet-gateway --region $REGION \
    --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=xfusion-igw}]' \
    --query 'InternetGateway.InternetGatewayId' --output text)
  aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $REGION
fi

Phase 3 — Public route table (→ IGW), associate with public subnet

PUB_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID --region $REGION \
  --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=xfusion-pub-rt}]' \
  --query 'RouteTable.RouteTableId' --output text)

aws ec2 create-route --route-table-id $PUB_RT \
  --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID --region $REGION

aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_SUBNET --region $REGION

This route table makes xfusion-pub-subnet genuinely public — its 0.0.0.0/0 points at the IGW. The NAT Gateway must live in this subnet so its own outbound traffic (forwarding on behalf of the private instance) can reach the internet via the IGW.

Phase 4 — Elastic IP + NAT Gateway

# Allocate an EIP for the NAT GW
NAT_EIP_ALLOC=$(aws ec2 allocate-address --domain vpc --region $REGION \
  --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=xfusion-nat-eip}]' \
  --query 'AllocationId' --output text)

# Create the NAT Gateway IN THE PUBLIC SUBNET
NATGW_ID=$(aws ec2 create-nat-gateway \
  --subnet-id $PUB_SUBNET \
  --allocation-id $NAT_EIP_ALLOC \
  --region $REGION \
  --tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=xfusion-natgw}]' \
  --query 'NatGateway.NatGatewayId' --output text)

# NAT GW takes ~1-2 min to become available — wait for it
# aws ec2 wait nat-gateway-available --nat-gateway-ids $NATGW_ID --region $REGION
STATE=""
until [ "$STATE" = "available" ]; do
  STATE=$(aws ec2 describe-nat-gateways --nat-gateway-ids $NATGW_ID --region $REGION \
    --query 'NatGateways[0].State' --output text)
  echo "$NATGW_ID: $STATE"
  [ "$STATE" = "available" ] || sleep 10
done

Two must-get-right points:

  • NAT GW goes in the PUBLIC subnet, not the private one. This trips people constantly. The NAT GW needs a path to the internet for the traffic it forwards, so it sits in the public subnet (which routes to the IGW). The private instance then routes to the NAT GW. Putting the NAT GW in the private subnet creates a routing loop with no internet path — nothing works.
  • A NAT GW requires an EIP (public NAT GWs). allocate-address → pass its allocation ID to create-nat-gateway. The EIP is the NAT GW's public-facing address that S3 sees.

Phase 5 — Private route table → NAT Gateway

# Find the route table serving the private subnet (explicit assoc, else main)
PRIV_RT=$(aws ec2 describe-route-tables \
  --filters "Name=association.subnet-id,Values=$PRIV_SUBNET" \
  --region $REGION --query 'RouteTables[0].RouteTableId' --output text)
if [ "$PRIV_RT" = "None" ] || [ -z "$PRIV_RT" ]; then
  PRIV_RT=$(aws ec2 describe-route-tables \
    --filters "Name=vpc-id,Values=$VPC_ID" "Name=association.main,Values=true" \
    --region $REGION --query 'RouteTables[0].RouteTableId' --output text)
fi

# Point the private default route at the NAT GW (create, or replace if one exists)
aws ec2 create-route --route-table-id $PRIV_RT \
  --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NATGW_ID --region $REGION 2>/dev/null || \
aws ec2 replace-route --route-table-id $PRIV_RT \
  --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NATGW_ID --region $REGION

This is the step that actually gives the private instance internet access: its subnet's 0.0.0.0/0 now flows to the NAT GW (--nat-gateway-id, not --gateway-id which is for IGWs). The instance keeps no public IP — outbound-only internet via NAT, which is exactly the private-instance pattern.

Verify

# NAT GW available
aws ec2 describe-nat-gateways --nat-gateway-ids $NATGW_ID --region $REGION \
  --query 'NatGateways[0].{State:State,Subnet:SubnetId,EIP:NatGatewayAddresses[0].PublicIp}'

# Private route table points at the NAT GW
aws ec2 describe-route-tables --route-table-ids $PRIV_RT --region $REGION \
  --query 'RouteTables[0].Routes[?DestinationCidrBlock==`0.0.0.0/0`]'

# Poll the bucket for the cron-uploaded test file (2-3 min after config)
for n in $(seq 1 8); do
  FILES=$(aws s3 ls s3://xfusion-nat-285654546/ --region $REGION 2>/dev/null)
  if [ -n "$FILES" ]; then echo "SUCCESS — bucket contents:"; echo "$FILES"; break; fi
  echo "waiting for cron upload... ($n)"; sleep 30
done

Want: NAT GW State: available with its EIP; the private route table showing 0.0.0.0/0 → nat-...; and a file appearing in the bucket within 23 minutes. The file appearing is the end-to-end proof — the private instance (no public IP) reached S3 entirely through the NAT Gateway.

NAT Gateway vs NAT Instance (the contrast)

NAT Gateway (this task) NAT Instance (earlier task)
Management Fully AWS-managed You run/patch an EC2 box
Source/dest check N/A (managed) Must disable manually
iptables / IP forwarding None You configure MASQUERADE + ip_forward
HA / scaling Automatic within AZ Single instance, you handle HA
Cost Higher hourly + data processing Just the EC2 instance

The NAT Gateway is the production-standard choice; the NAT instance exists mostly for cost-sensitive or learning scenarios. This task is the "right way."

Debug if the file doesn't appear

  1. NAT GW stuck in pending → wait longer (up to 2 min), or it failed (usually the EIP was already in use — allocate a fresh one).
  2. Private route wrong → confirm 0.0.0.0/0 points at nat-... (NAT GW), not igw-.... A private subnet routing to an IGW directly doesn't work without a public IP on the instance.
  3. Public subnet's route table missing the IGW route → the NAT GW itself can't reach the internet, so forwarded traffic dies. Confirm xfusion-pub-rt has 0.0.0.0/0 → igw-... and is associated with the public subnet the NAT GW lives in.
  4. Give it the full 23 min — the cron runs on an interval; the file won't appear instantly even once networking is correct.