Layer 4 - envs: One Root per Environment

Each of envs/dev, envs/staging, envs/prod is a root that assembles the modules into one complete environment, with its own state in its own account. The three are near-identical in structure but differ in inputs (dev runs lean; prod runs HA).

envs/dev/main.tf is the assembly. It's worth seeing how modules are wired together by passing one module's outputs as another's inputs:

HCL
module "networking" { source = "../../modules/networking"  ... }

module "rds" {
  source          = "../../modules/rds"
  vpc_id          = module.networking.vpc_id          # output of networking → input of rds
  data_subnet_ids = module.networking.data_subnet_ids
  ...
}

module "ecs" {
  source               = "../../modules/ecs"
  app_subnet_ids       = module.networking.app_subnet_ids
  db_secret_arn        = module.rds.db_secret_arn       # output of rds → input of ecs
  redis_endpoint       = module.redis.redis_primary_endpoint
  ...
}

This network of references is the dependency graph: because ecs consumes rds's output, OpenTofu automatically creates the database before the app. You never order modules manually - the references do it.

Passing aliased providers to a module

Recall the second us_east_1 provider from section 5.7. The edge module needs to create CloudFront certificates in us-east-1, so the root explicitly hands it both providers:

HCL
module "edge" {
  source = "../../modules/edge"
  providers = {
    aws           = aws            # the default (ca-central-1) provider
    aws.us_east_1 = aws.us_east_1  # the us-east-1 one
  }
  ...
}

The providers = { ... } block maps the root's providers to the names the module expects. This is how a single root can build resources across two regions.

What differs between environments

The structure is the same across dev/staging/prod; the inputs differ. Look at envs/dev/variables.tf: dev defaults to az_count = 2, single_nat_gateway = true, enable_interface_endpoints = false - the lean, cheap profile. Prod's variables default to the HA profile. The actual values for a specific deployment come from a terraform.tfvars file you create from the committed envs/dev/terraform.tfvars.example:

HCL
region            = "ca-central-1"
vpc_cidr          = "10.10.0.0/16"
az_count          = 2
state_bucket_name = "alphaform-dev-tofu-state"   # used to find the backend at init
allowed_account_ids = ["000000000000"]           # the real dev account ID (guardrail)

The root's envs/dev/outputs.tf re-exports the useful endpoints (VPC ID, DB endpoint, frontend URLs, the alarm topic, etc.) so a human can read them after apply with tofu output.

Adesh Tamrakar
SOFTWARE ENGINEER · VAULT

Notes, insights and random discoveries from a working engineer's vault - written for future me, published for you.