Learning objectives
- Allocate six non-overlapping subnets from one VPC CIDR.
- Keep resource addresses stable with AZ-ID keys and
for_each. - Review the public, private, NAT, endpoint, and flow-log graph before any apply.
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.
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
| Path | Route and purpose |
|---|---|
| Public subnet | Default route to the internet gateway. It contains NAT gateways and later public load balancers. |
| Private subnet | Default route only to NAT. There is no private subnet route directly to an internet gateway. |
| Learning profile | One NAT gateway lowers cost, but one-AZ NAT egress is not production resilient. |
| Production profile | One NAT gateway per AZ retains private egress through an AZ impairment. |
| Endpoints | Private 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
- Expect exactly three public and three private
aws_subnetresources and six distinct CIDRs. - Expect the EKS tags
kubernetes.io/role/elb=1andkubernetes.io/role/internal-elb=1. - For learning, expect one EIP and one NAT gateway. For production, expect three of each, aligned with AZ keys.
- Check every
aws_route.private_defaulthasnat_gateway_id, nevergateway_id. - Check interface endpoints use private subnets, private DNS, and the endpoint security group. Check flow logs have the intended retention.
Failure drill
Verify without applying
- Run the module's native mocked tests with
tofu test -test-directory=testsand the matching supported Terraform command. - Run
terraform fmt -check -recursive learn-terraform/capstoneandtofu fmt -check -recursive learn-terraform/capstone. - Inspect the saved plan or graph for the route boundaries above. Remove local
.terraformdirectories 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.