Terraform Associate study materials
Terraform Associate study resources

One Root Module, Three Workspaces, Forty Resources

Terraform Associate · Put your knowledge to work in context.

All case studies

One Root Module, Three Workspaces, Forty Resources

A team's whole AWS footprint - VPC, ECS, RDS, 40+ resources - lives in a single 2,400-line root module, selected by terraform workspace for dev, staging and prod. Environment differences are expressed inline: count = terraform.workspace == "prod" ? 3 : 1 for NAT gateways, and eight private subnets built with count.index and cidrsubnet. Last week an engineer deleted one entry from the subnet CIDR list to free address space; plan showed five subnets destroyed and five created in staging, and the RDS subnet group churned for 40 minutes. Leadership now wants modules, but nobody agrees on what that means. Two engineers also apply locally, and staging carries an undocumented security group rule prod does not have.

1. Explain why deleting one list element recreated five subnets, and the change that makes the next such edit boring.

Read the worked answer

count is positional. The addresses are aws_subnet.private[0] … [7], and each one's identity in Terraform is its index, not its CIDR. Delete the element at index 3 and every later element shifts down: address [3] now describes what used to be [4], and so on. Since cidr_block is a force-new attribute, Terraform sees five addresses whose content changed incompatibly and plans destroy/create for each. The RDS subnet group references those subnet IDs, so it is dragged along.

The fix is for_each over a map with stable keys, e.g. { use1-az1 = "10.0.1.0/24", use1-az2 = "10.0.2.0/24" }. Addresses become aws_subnet.private["use1-az1"], and removing a key affects exactly that one subnet.

Migration is not free, though. Indexes and keys do not match, so you need one moved block per subnet (from = aws_subnet.private[0], to = aws_subnet.private["use1-az1"]) or the equivalent terraform state mv calls. Do the migration as its own change with an empty plan at the end, before anyone touches the CIDR list again.

2. The team says it wants modules. Distinguish the two things they might mean, and give a safe migration order.

Read the worked answer

Two different words are in play:

  • Composition - separate root modules per environment in envs/dev, envs/staging, envs/prod, each with its own state. This fixes the workspace-conditionals and the shared-configuration problem.
  • Reusable child modules - modules/network, modules/service, called with inputs and returning outputs. This fixes the 2,400-line file and duplicated logic.

Do composition first, because extracting a module from a root that still branches on workspace locks the branch logic into the module interface.

Safe order:

  1. Copy the current root into envs/staging, change nothing, prove plan is empty.
  2. Repeat for dev and prod, pointing each at its own state key. Migrate state with terraform init -migrate-state per directory.
  3. Replace the workspace conditionals with variables and *.tfvars per environment, still producing identical plans.
  4. Extract one leaf module (network) with no behaviour change; use moved blocks or state mv so addresses survive.
  5. Only then tackle the app tier.

Never combine a refactor with a provider major-version bump in one PR - you lose the ability to tell which change caused a diff.

3. Is `terraform workspace` the right environment separator here? Argue both sides and state what you would recommend.

Read the worked answer

For: one configuration, one backend, one place to change code; cheap to add an ephemeral environment; terraform.workspace is available for small variations.

Against, and stronger here: workspaces share one configuration, one backend prefix and one set of credentials. They suit environments that are identical in shape - ephemeral review environments, test fixtures. This team's environments are structurally different: prod has three NAT gateways, staging has an undocumented security group rule, and presumably different accounts. Encoding that in terraform.workspace == expressions puts environment topology inside resource bodies, where it cannot be reviewed per environment, cannot be type-checked, and quietly defaults to the non-prod branch if someone forgets workspace select.

Recommendation: directory-per-environment root modules with separate state keys and separate credentials, workspace use reserved for throwaway review environments. The concrete failure mode they already have is evidence: a local apply against the wrong selected workspace silently produced drift in staging, and nothing in the code prevented it. Separating directories makes terraform apply in the wrong environment a visible, reviewable act.