State, Pinning & Gotchas
The earlier lessons showed the happy path: add a map entry, plan, apply. This one is the sharp edges — the handful of things that, done casually, cause data loss, drift, or an unreviewable diff. None of them is exotic; all of them come down to respecting how Terraform tracks reality.
State is the map between config and reality
Terraform's state records which real Azure resource corresponds to which configuration
address (module.storage["reporting"], and so on). It's how plan computes a diff without
re-reading everything, and it's why two things are true:
- State is stored remotely and locked. It lives in the HUB storage account (see Environments & Deployment), so the whole team and the pipeline share one truth, and locking stops two applies from corrupting it. You never edit it by hand.
- State is per-workspace. Each environment's state is independent. A resource "missing" from a plan is often just the wrong workspace selected.
Renaming a live for_each key is surgery, not a text edit
This is the gotcha that bites hardest, because it looks harmless. The map key is part of the
resource's state address, so renaming reporting → reports in a locals map doesn't
rename anything — Terraform reads it as destroy module.storage["reporting"], create
module.storage["reports"]. For a stateful resource that means data loss.
When you genuinely must rename, tell Terraform the identity is the same, either with a
moved block in code:
moved {
from = module.storage["reporting"]
to = module.storage["reports"]
}
…or with a one-off terraform state mv
command. Either way the plan should then show no destroy/create for that resource — only the
address moving. If you still see a destroy, stop.
plan output is easy to skim as "looks fine." Read it for the word destroy before anything
else. An unexpected destroy is nearly always one of three things: a renamed for_each key, a
changed key set on a for_each, or a module upgrade that moved a resource internally. Each has
a safe path (a moved block, or a deliberate migration); none should be applied blind.
Everything is pinned, on purpose
Reproducibility here is not luck — it's pinning at three levels, and loosening any of them
trades a surprise-free plan for a surprise:
- Module sources —
?ref=v1.0.1locks each module to an exact release. Never widen a pin to a branch or omit the ref (see Reusable Modules). - Providers —
terraform.tfpinsazurerm,azuread,azapi, and the rest to exact versions. A provider bump can change resource behavior; treat it as its own reviewed change. - Terraform itself —
required_version = "1.11.3". The pipeline installs that version, so local work should match it.
Upgrading any of these is a normal, welcome activity — but a deliberate one: bump the
version, read the changelog, plan, and inspect the diff. The rule from the repo's own guidance
is to keep pins unless the task is the upgrade.
The validation workflow, every time
Before anything reaches a pipeline, run the loop from alertca/:
terraform fmt -recursive # canonical formatting
terraform init # backend + modules
terraform validate # syntax and internal consistency
terraform workspace select dev
terraform plan -var-file=./envs/dev.tfvars # the review
validate catches structural errors cheaply; plan is the real review. Neither touches Azure
except to read — nothing changes until an approved apply.
Curated references
For the topics that reward going deeper than this section does:
- State — State · Backends · Workspaces
- Refactoring safely —
movedblocks ·terraform state mv· Importing existing resources - Style & modules — Terraform style guide · Module sources
- Azure — azurerm provider · Well-Architected Framework
Wrapping up
The sharp edges all reduce to one habit: respect state. It's remote, locked, and per-workspace;
renaming a live for_each key is a moved/state mv operation, not a text edit; sources,
providers, and Terraform are pinned so plan stays reproducible; and every change is reviewed
as a plan you actually read — destroy lines first. Follow those and the stack stays as
predictable at 200 resources as at two.
That completes the Terraform section — from
the shape of the stack to the operations that keep it safe. For the exact,
always-current implementation details, the repo's AGENTS.md cookbooks and each module's
README.md are the living reference.