Chapter 11 of 24

Learning and Production Profiles

One HCL tree, two deliberate input profiles, and a plan review that makes cost and recovery trade-offs visible.

Learning objectives

Profiles change inputs, not ownership

The foundation module stays shared. The learning profile uses one NAT gateway, seven-day flow-log retention, and selects a $10 account-wide monthly alert threshold. The production profile uses one NAT gateway per AZ, 365-day flow-log retention, protected logs, and selects a $100 account-wide monthly alert threshold. If both profiles are deployed in the same AWS account, both budgets watch the same total account spend and overlap; they are configuration-selected alerts, not per-profile cost attribution. These are visible trade-offs, not a hidden production mode.

ControlLearningProduction
NAT gateways and EIPsOne, lower cost and one-AZ egress dependency.Three, aligned to each AZ.
Flow logsSeven days, removable in a disposable account.365 days, retained when Terraform removes the stack.
StateDistinct backend key.Distinct backend key and a protected manual CI gate.

Account and region remain fixed controls

Both profiles run in ap-south-1, with a named profile and an expected account ID. A production profile is not a replacement for account separation: use an account boundary when the ownership, blast radius, or billing boundary requires one. Do not set a different profile merely to make a plan succeed.

Lab: compare plan output

export COURSE_ROOT="$PWD/learn-terraform/capstone"
export COURSE_AWS_PROFILE="replace-with-approved-aws-profile"
export COURSE_AWS_ACCOUNT_ID="123456789012"
export COURSE_AWS_REGION="ap-south-1"
: "${COURSE_BUDGET_ALERT_EMAIL:?export a real budget alert email first}"
export TF_VAR_budget_alert_email="$COURSE_BUDGET_ALERT_EMAIL"

# Confirm the named profile, account, and region before either init.
ACTUAL_AWS_ACCOUNT_ID="$(aws --profile "$COURSE_AWS_PROFILE" --region "$COURSE_AWS_REGION" sts get-caller-identity --query Account --output text)"
test "$ACTUAL_AWS_ACCOUNT_ID" = "$COURSE_AWS_ACCOUNT_ID"
export LEARNING_DIR="$(mktemp -d)"
export PRODUCTION_DIR="$(mktemp -d)"
cp -R "$COURSE_ROOT/infra" "$LEARNING_DIR/infra"
cp -R "$COURSE_ROOT/infra" "$PRODUCTION_DIR/infra"

# Terraform in learning.
cd "$LEARNING_DIR/infra/stacks/foundation"
cp ../../environments/learning/foundation.backend.hcl.example foundation.backend.hcl
# Edit the bucket and kms_key_id placeholders in this untracked copy.
terraform init -reconfigure -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
terraform validate
terraform plan -input=false -var-file=../../environments/learning/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE" -var="expected_aws_account_id=$COURSE_AWS_ACCOUNT_ID" -out=tfplan

# OpenTofu in the separate production copy.
cd "$PRODUCTION_DIR/infra/stacks/foundation"
cp ../../environments/production/foundation.backend.hcl.example foundation.backend.hcl
# Edit the bucket and kms_key_id placeholders in this untracked copy.
tofu init -reconfigure -backend-config=foundation.backend.hcl -backend-config="profile=$COURSE_AWS_PROFILE"
tofu validate
tofu plan -input=false -var-file=../../environments/production/foundation.tfvars -var="aws_profile=$COURSE_AWS_PROFILE" -var="expected_aws_account_id=$COURSE_AWS_ACCOUNT_ID" -out=tfplan

The paired CLI commands demonstrate alternatives, not permission to mix local state. This is plan-only and creates no AWS resources.

Review the difference

  1. Learning proposes one EIP and one NAT gateway; production proposes three of each.
  2. Production tags and flow-log retention must show the production profile, not a copied learning value.
  3. Confirm the expected account precondition and region before comparing counts.
  4. Read KMS and state protections: the production root cannot casually remove retained recovery data.
  5. Check the budget amount and alert recipient are approved for that account.

Failure drill

Do not make production look like learning by editing the plan after review. A single-NAT production plan may pass validation while introducing an AZ-coupled outage path. If a production plan unexpectedly resembles learning, stop, inspect the selected .tfvars and backend key, then create a new reviewed plan.

Verify without applying

  1. Run both recursive format checks from Chapter 9.
  2. Compare planned NAT and EIP addresses, not only the summary line.
  3. Check the production flow-log group has its retention and non-removal setting.
  4. Confirm the backend examples have different keys before initializing either profile.

Cleanup

Delete only $LEARNING_DIR and $PRODUCTION_DIR. Do not run a cloud cleanup command from a plan-only exercise. In a separately approved disposable apply, remove dependent states first, then inspect NAT gateways, EIPs, endpoint ENIs, retained logs, secret recovery windows, and KMS protections before accepting the final result.

Quick check

Why not use Terraform workspaces here?

Separate backend keys and copied working directories make the profile and state boundary visible, including when Terraform and OpenTofu are both taught.

Does a budget alert prevent a charge?

No. It warns after the configured threshold; plan review and lifecycle controls prevent unintended work earlier.

Recap and next

A production profile is a reviewed set of resilience, retention, and cost choices. Keep it isolated, explicit, and verified from the plan.

Next: place the same controls in an opt-in GitLab CI workflow that never stores long-lived AWS keys.