Software & TechnicalFree
Terraform Documentation and Blast Radius
River reads the repository and states, per variable, how many resources an apply replaces, and which other configurations consume the outputs.
Terraform's own module guidance settles the obvious half of this question. The README does not need to document inputs or outputs, because tooling will automatically generate this, and every answer on page one for this query is about generating that table well. So the table gets generated, in CI, on every commit, and it still does not answer the one thing an engineer wants at the moment they open a pull request. If I change this value, what does the apply destroy?
Cawdale runs a B2B payments platform out of one Terraform monorepo: 41 modules, 214 input variables, 96 outputs, six state files, and generated documentation on 38 of the 41. Crossed against the provider schemas, 23 of those 214 variables reach an argument that forces replacement. Two of the 23 say so. The change that exposed it was a one-word diff, a name prefix going from cawdale to cawdale-prod, approved in four minutes, replacing nine of that module's twenty-four resources.
Built for whoever is about to approve an infrastructure change, and for the engineer who inherited a repository nobody documented. Run it on a monorepo, on one module before you publish it, or on the state you were handed. Given an open pull request it answers for that change first, before the register. What the estate contains is an inherited architecture reconstruction, what a change costs is a cloud cost analysis, and how that change actually reaches production once it is approved is a separate release process this register does not track.
Three things the generated table has no source for
Terraform already owns the vocabulary. A plan marks a replacement with a symbol of its own, and the documentation is precise about it: the replace action destroys the resource and then recreates it, and the line responsible carries a forces replacement comment. The trouble is when that comment appears. Plan time is after review, after merge, and often after somebody has typed yes. The provider schema knew at authoring time, so crossing a variable against the arguments it reaches turns a surprise into a property of the input.
The second is who is downstream. A documentation generator reads one directory, and so does a plan. But the remote state data source reads the root module output values from some other Terraform configuration. A module's outputs are therefore read by configurations that are not in the plan, and not in the state that plan compared against. Cawdale's platform-base module has fourteen consumers. Five live in three other repositories, so the plan the reviewer read covered nine.
Third is which replacements are not replacements. Of Cawdale's nine, two do not come back the way they went. One is an access-log bucket holding 4.1 TB, whose name is globally unique and whose destroy fails outright while objects are still in it. The other is the primary database, whose last rehearsed restore took three hours forty. The same crossing runs the other way. Thirty-one of the 96 outputs have no consumer anywhere, and five modules are called by nothing.
How it works
Bring the repository
The Terraform or OpenTofu tree, including the root configurations, not only the modules directory.
Add the state
State files or the backend holding them, since consumers outside this repository only appear there.
Read the blast radius
Per variable: the arguments it reaches, the resources it replaces, and which of those never come back.
Review against it
Open the register beside the pull request, so a one-word diff arrives with its count attached.
What you get
- A blast radius per variable: arguments reached, resources touched, and how many an apply replaces
- Replacements traced one hop on, through the dependencies that force a second resource to go
- Irrecoverable replacements marked apart, because a restore and a rebuild are not the same risk
- Every consumer of every output, including the remote-state reads living in other repositories
- Outputs nobody reads and modules nothing calls, which is the deletion nobody dares make
- A module register carrying variables, outputs, consumers and worst blast radius, one row each
- The resource dependency graph drawn from the code, not from the diagram somebody remembers
Common questions
We already run terraform-docs in CI.
Then you have the inputs and outputs table, and this does not reproduce it. It adds the column that table has no source for, which is what changing each value does to the resources it feeds. Cawdale's generated tables listed all 214 variables and were entirely correct, and the 21 undocumented replacement-forcing ones read exactly like the other 193.
Cannot the reviewer just run a plan?
A plan is the right answer, arriving too late and too narrow. Too late because on most teams it runs after review rather than before it. Too narrow because it covers the state it was run against, and Cawdale's covered nine of fourteen consumers. The other five sat in state files in other repositories.
How do you know a variable forces replacement?
From the provider schema rather than from the description somebody wrote. Each argument declares whether a change needs a new resource, so following a variable through the expressions it feeds gives every argument it can reach, and the schema says which of those are one-way. That makes it a lookup rather than a judgement call.
We are on OpenTofu, or on CDK.
OpenTofu takes the same path, because the provider schemas and the state format are the same ones. For CDK and Pulumi the crossing runs against the synthesised configuration and the preview instead, which carry the same replacement information, and the register records which of the two it actually read.
What about modules from the public registry?
Read the same way and marked as not yours to change, which matters because their variables have the same property yours do. A registry module's blast radius belongs to whoever pins the version, so the register carries the pinned version beside it and a bump gets reviewed as a change rather than waved through as an upgrade.
Does this replace an architecture diagram?
No, and the two answer different questions. A diagram says what exists; this says what a change does to it. The estate itself and who owns each part of it is an inherited architecture reconstruction, and the argument behind a module's shape belongs in an architecture decision record.
What do we actually get back?
A sheet with one row per module carrying its variables, outputs, consumers and worst blast radius, a document walking each module's dependencies and what an apply to it replaces, and a dependency graph drawn from the code. Cawdale's summary line was nine replaced, two irrecoverable, five consumers outside the plan. Whether a module's default tags reach the resources it creates is a separate cost allocation review.
Terraform Documentation and Blast Radius
Fill in the form and your workspace opens with the work already underway.