Skip to content

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.

TerraformPlanParser (opentremor_analyzer_terraform_plan/parser.py) works in two passes rather than one streaming scan:

  1. 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.
  2. 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.

Phrase in the headeraction
will be createdcreate
will be destroyeddestroy
will be updated in-placeupdate
will be replaced / must be replacedreplace
is tainted, so must be replacedreplace
will be read during applyread
has moved to <address>move

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:

Addresstypename
aws_security_group.webaws_security_groupweb
module.network.aws_subnet.privateaws_subnetprivate
data.aws_ami.latestaws_amilatest

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

{
"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.

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

Rules ship as plain Markdown in this package’s own opentremor_analyzer_terraform_plan/rules/ directory:

FileServed at
generic-rules.mdGET /terraform-plan/rules
aws-rules.mdGET /terraform-plan/rules?type=aws

Drop in a new {type}-rules.md file and it’s picked up automatically — no registration step.

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.
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 }
}
Terminal window
terramate run terraform show -no-color tfplan > full_plan.txt

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