Chapter 9 of 24 · review cost

Three-AZ Networking

Use one HCL tree to build a VPC with public ingress, private workloads, controlled egress, endpoints, and flow logs.

Learning objectives

CIDR and stable keys

cidrsubnet("10.42.0.0/16", 4, 0) produces 10.42.0.0/20. The module assigns the six supplied subnet numbers to stable AZ-ID keys, so resource addresses do not change when an AWS account maps an AZ name differently.

An AZ name is account-specific. An AZ ID identifies the physical zone. This capstone uses Mumbai ap-south-1 IDs aps1-az1, aps1-az2, and aps1-az3. The root resolves their account-specific names through aws_availability_zones at plan time.

Routes, endpoints, and cost

PathRoute and purpose
Public subnetDefault route to the internet gateway. It contains NAT gateways and later public load balancers.
Private subnetDefault route only to NAT. There is no private subnet route directly to an internet gateway.
Learning profileOne NAT gateway lowers cost, but one-AZ NAT egress is not production resilient.
Production profileOne NAT gateway per AZ retains private egress through an AZ impairment.
EndpointsPrivate DNS sends supported AWS service traffic to interface endpoints. The S3 gateway endpoint adds private route-table entries.

IPv6 is intentionally not enabled in this first network state. Adding it later needs a VPC IPv6 CIDR, subnet allocation, egress-only routing, security review, and workload testing. It is not a substitute for reviewing IPv4 routes.

Lab: inspect the shared network tree

The network module owns VPC children. The foundation root supplies providers, default tags, AZ-ID resolution, a partial S3 backend, and only later-state outputs. The profiles change inputs, not module code. Terraform and OpenTofu are mutually exclusive alternatives for the same learning profile: use a fresh copy for each, never one after the other in the same directory.

export COURSE_ROOT="$PWD/learn-terraform/capstone"
export COURSE_AWS_PROFILE="replace-with-approved-aws-profile"

# Choose Terraform for the learning profile.
export IAC_TOOL=terraform
export LEARNING_DIR="$(mktemp -d)"
cp -R "$COURSE_ROOT/infra" "$LEARNING_DIR/infra"
cd "$LEARNING_DIR/infra/stacks/foundation"
cp ../../environments/learning/foundation.backend.hcl.example foundation.backend.hcl
# Replace the bucket, key, and KMS ARN placeholders in foundation.backend.hcl.
terraform init -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
terraform validate
aws --profile "$COURSE_AWS_PROFILE" --region ap-south-1 sts get-caller-identity --query '{Account:Account,Arn:Arn,Region:`ap-south-1`}' --output table
terraform plan -var-file=../../environments/learning/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE"

# Or choose OpenTofu for the same learning profile in a different directory.
export IAC_TOOL=tofu
export TOFU_LEARNING_DIR="$(mktemp -d)"
cp -R "$COURSE_ROOT/infra" "$TOFU_LEARNING_DIR/infra"
cd "$TOFU_LEARNING_DIR/infra/stacks/foundation"
cp ../../environments/learning/foundation.backend.hcl.example foundation.backend.hcl
tofu init -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
tofu validate
aws --profile "$COURSE_AWS_PROFILE" --region ap-south-1 sts get-caller-identity --query '{Account:Account,Arn:Arn,Region:`ap-south-1`}' --output table
tofu plan -var-file=../../environments/learning/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE"

# Production is isolated and plan-only. Use the CLI selected above.
export PRODUCTION_DIR="$(mktemp -d)"
cp -R "$COURSE_ROOT/infra" "$PRODUCTION_DIR/infra"
cd "$PRODUCTION_DIR/infra/stacks/foundation"
cp ../../environments/production/foundation.backend.hcl.example foundation.backend.hcl
"$IAC_TOOL" init -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
"$IAC_TOOL" validate
aws --profile "$COURSE_AWS_PROFILE" --region ap-south-1 sts get-caller-identity --query '{Account:Account,Arn:Arn,Region:`ap-south-1`}' --output table
"$IAC_TOOL" plan -var-file=../../environments/production/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE"

Plan needs an approved named AWS SSO profile because the root reads available AZ metadata. The command passes that profile to both the backend and provider and confirms the account in ap-south-1 immediately before planning. Production has no apply command. If backend settings must be read again in an isolated copy, init -reconfigure discards only cached backend metadata; init -migrate-state moves state and needs a backup plus reviewed ownership decision. Do not use either to switch CLIs in a shared directory: keep their .terraform directories and lock files isolated. Never add access keys to HCL, variable files, or this repository.

Review the plan

  1. Expect exactly three public and three private aws_subnet resources and six distinct CIDRs.
  2. Expect the EKS tags kubernetes.io/role/elb=1 and kubernetes.io/role/internal-elb=1.
  3. For learning, expect one EIP and one NAT gateway. For production, expect three of each, aligned with AZ keys.
  4. Check every aws_route.private_default has nat_gateway_id, never gateway_id.
  5. Check interface endpoints use private subnets, private DNS, and the endpoint security group. Check flow logs have the intended retention.

Failure drill

A plan can be wrong while still succeeding. A data source can return a different account's AZ-name mapping, a NAT can land in the wrong public subnet, or a route can send a private workload to the internet gateway. Read resource addresses and route targets. Do not apply to discover this.

Verify without applying

  1. Run the module's native mocked tests with tofu test -test-directory=tests and the matching supported Terraform command.
  2. Run terraform fmt -check -recursive learn-terraform/capstone and tofu fmt -check -recursive learn-terraform/capstone.
  3. Inspect the saved plan or graph for the route boundaries above. Remove local .terraform directories and generated graph files afterward.

Cleanup

Do not apply this foundation casually: NAT gateways, public IPv4 addresses, endpoints, and logs cost money. If an approved disposable apply exists, destroy dependent states first, then this foundation. Confirm retained EIPs, NAT gateways, ENIs, and the production flow-log group deliberately. The production profile protects the log group from Terraform deletion, so it may need an explicit retention decision.

Quick check

Why use AZ IDs as keys?

The stable physical-zone identifier avoids treating account-local AZ letters as durable infrastructure identity.

Why does a private subnet need NAT rather than the internet gateway?

An internet gateway does not provide source-address translation for private IPv4 instances. NAT provides controlled outbound egress without a direct private route to the gateway.

Recap and next

The foundation separates public ingress from private workload routing, makes the egress cost and resilience trade-off explicit, and exports only VPC, subnet, AZ, and endpoint-security-group identifiers. Next, add IAM, encryption, and network access controls.

Next: Chapter 10 will secure the foundation without changing its network ownership boundary.