Terraform Plan Analyzer
URL slug: terraform-plan
Takes the text output of terraform plan — a single stack or a Terramate
run that concatenates several — and turns every resource change comment
into its own analysis unit.
Parsing strategy
Section titled “Parsing strategy”TerraformPlanParser (opentremor_analyzer_terraform_plan/parser.py) works
in two passes rather than one streaming scan:
- Locate every header line first. A header is a
# <address> <verb phrase>comment where the verb phrase matches one of Terraform’s known action wordings (see below). This is checked in one combined regex match per line, so a header’s address and action are captured together instead of being extracted separately later. - Slice bodies between headers. Each unit’s body runs from its header line up to the next header — or up to an earlier
Plan: N to add...summary line if one appears first, since that always closes out the current stack’s section.
A shared lookup table drives both “is this really an action header” (step 1)
and “what action does it describe” (the action field) — so recognising a
line and classifying it can’t drift out of sync with each other.
Recognised action wording
Section titled “Recognised action wording”| Phrase in the header | action |
|---|---|
will be created | create |
will be destroyed | destroy |
will be updated in-place | update |
will be replaced / must be replaced | replace |
is tainted, so must be replaced | replace |
will be read during apply | read |
has moved to <address> | move |
Address parsing
Section titled “Address parsing”type/name come from the header’s address token, not the resource body —
this keeps data sources and module-qualified addresses correct without any
special-casing:
| Address | type | name |
|---|---|---|
aws_security_group.web | aws_security_group | web |
module.network.aws_subnet.private | aws_subnet | private |
data.aws_ami.latest | aws_ami | latest |
Terramate stacks
Section titled “Terramate stacks”For concatenated multi-stack output, the parser makes a separate pass that
maps each header’s line number to whichever terramate: Entering stack in <path> line most recently preceded it — kept independent from the
body-slicing pass above so stack bookkeeping can’t interfere with where a
block starts or ends.
terramate: Entering stack in /terraform/platform/network/staging/eu-west-1terramate: Executing command "terraform show -no-color tfplan"
Terraform will perform the following actions:
# aws_iam_role.ci_deploy must be replaced ...
Plan: 1 to add, 0 to change, 1 to destroy.terramate: Entering stack in /terraform/platform/network/prod/global...
# aws_iam_user.service_account will be created ...
Plan: 2 to add, 0 to change, 0 to destroy.terramate: Entering stack in /terraform/platform/tooling/staging/eu-west-1...
No changes. Your infrastructure matches the configuration.Three units come out of this: one from the first stack, two from the second. The third stack reports no changes, so it contributes nothing.
Unit shape
Section titled “Unit shape”{ "hash": "<sha256 of the normalised body>", "type": "aws_security_group", "name": "web", "action": "update", "body": " # aws_security_group.web will be updated in-place\n ~ resource ...", "metadata": { "generated_at_utc": "2026-04-28T10:30:00Z", "source_path": null, "group_path": "/terraform/tooling/network/dev/eu-west-1" }}group_path stays null for plain, non-Terramate terraform plan output.
Content-addressed hashing
Section titled “Content-addressed hashing”Hashing goes through OpenTremor Core’s libs/hash.py, against a normalised
copy of the body — a few ephemeral-value patterns are stripped first so a
second plan run with different placeholder text still hits the same hash:
| Stripped before hashing | Looks like |
|---|---|
(known after apply) | id = (known after apply) |
(sensitive value) | password = (sensitive value) |
(will be computed) | arn = (will be computed) |
| blank lines | — |
A stable hash means re-running the same plan doesn’t re-run the LLM — the prior analysis is reused. It also means two textually identical resource bodies in different stacks intentionally share one stored analysis.
Rulesets
Section titled “Rulesets”Rules ship as plain Markdown in this package’s own
opentremor_analyzer_terraform_plan/rules/ directory:
| File | Served at |
|---|---|
generic-rules.md | GET /terraform-plan/rules |
aws-rules.md | GET /terraform-plan/rules?type=aws |
Drop in a new {type}-rules.md file and it’s picked up automatically — no
registration step.
Stack paths in reports
Section titled “Stack paths in reports”Both the Markdown and HTML reports (rendered by OpenTremor Core) surface
group_path when it’s present:
- Each resource’s own section gets a
Stack: /terraform/...line under its action badge. - The findings summary table gains a Stack column automatically once at least one resource carries a path — it’s left out entirely for single-stack plans.
Worked examples
Section titled “Worked examples”A single-stack plan
Section titled “A single-stack plan”Terraform will perform the following actions:
# aws_db_instance.orders will be created + resource "aws_db_instance" "orders" { + identifier = "orders-primary" + storage_encrypted = false }
Plan: 1 to add, 0 to change, 0 to destroy.Produces one unit with group_path: null:
{ "hash": "d4e8b1...", "type": "aws_db_instance", "name": "orders", "action": "create", "body": "...", "metadata": { "generated_at_utc": "...", "source_path": null, "group_path": null }}A Terramate run, piped straight in
Section titled “A Terramate run, piped straight in”terramate run terraform show -no-color tfplan > full_plan.txtNo preprocessing needed — ingest full_plan.txt as-is and get back one
unit per changed resource across every stack, each carrying the stack path
it came from.