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:
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:
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:
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.