Terraform Code-Change Analyzer
URL slug: terraform-code-change
Takes a unified git diff covering Terraform (.tf) files and turns each
changed block into its own analysis unit — enough for an LLM to catch
things like a raw resource "aws_db_instance" showing up where an approved
module should have been used instead.
Parsing strategy
Section titled “Parsing strategy”TerraformCodeChangeParser (opentremor_analyzer_terraform_code_change/parser.py)
streams the diff hunk by hunk rather than building the whole diff into
memory first:
- Walk the diff, yielding one hunk at a time.
diff --git a/... b/...headers mark file boundaries;.tffiles are kept, anything else is dropped before a single hunk line is even looked at. - Within a hunk, find block-opening lines (
resource,module,data,variable,output,locals,terraform,provider) and locate where each one closes by tracking brace balance forward from the opening line. - Keep only blocks with a real change — a block with nothing but context (
-prefixed) lines is discarded;+/-lines that never matched a recognised block shape (a changed comment, for instance) still get folded into a fallback unit so the change isn’t silently dropped.
Block-to-identity mapping
Section titled “Block-to-identity mapping”| Block opens with | type | name |
|---|---|---|
resource "aws_db_instance" "orders" | aws_db_instance | orders |
module "rds_postgres" | module | rds_postgres |
data "aws_ami" "latest" | aws_ami | latest |
variable "env" | variable | env |
output "db_endpoint" | output | db_endpoint |
provider "aws" | provider | aws |
locals / terraform | same as keyword | same as keyword |
Action from diff markers
Section titled “Action from diff markers”| Markers present in the block | action |
|---|---|
+ only | add |
- only | delete |
| both | modify |
Diff markers stay in the body
Section titled “Diff markers stay in the body”body keeps its original +/-/ prefixes rather than stripping them —
an LLM rule that cares about what changed (e.g. “was storage_encrypted = false just added, or was it already there?”) needs that distinction, and
losing the markers would make added and pre-existing lines indistinguishable.
Unit shape
Section titled “Unit shape”{ "hash": "<sha256 of the normalised body>", "type": "aws_db_instance", "name": "orders", "action": "add", "body": "+resource \"aws_db_instance\" \"orders\" {\n+ identifier = \"orders-primary\"\n+}", "metadata": { "generated_at_utc": "2026-06-22T10:30:00Z", "source_path": "infra/rds.tf", "group_path": null }}source_path comes from the diff header’s b/... side. group_path is
always null here — it’s a plan-analyzer concept (Terramate stacks) this
analyzer has no equivalent of.
Hashing and dedup
Section titled “Hashing and dedup”Hashing runs through the same libs/hash.py normalisation used by the plan
analyzer — identical blocks land on the same hash and reuse whatever
analysis was already stored for it.
Rulesets
Section titled “Rulesets”Rules live as Markdown in this package’s own
opentremor_analyzer_terraform_code_change/rules/ directory:
| File | Served at |
|---|---|
code-change-rules.md | GET /terraform-code-change/rules |
{type}-code-change-rules.md | GET /terraform-code-change/rules?type={type} |
What the default ruleset covers
Section titled “What the default ruleset covers”| Rule ID | Area | What it catches |
|---|---|---|
| TFCC-001 | Module compliance | a raw resource used where an approved module exists |
| TFCC-002 | Module compliance | module source isn’t the approved registry/repo |
| TFCC-010 | Resource hygiene | missing required tags/labels |
| TFCC-011 | Resource hygiene | hardcoded value instead of a variable |
| TFCC-020 | Security regression | the diff itself weakens security posture |
| TFCC-021 | Security regression | a plaintext secret was added |
| TFCC-030 | General hygiene | a Terraform best-practice violation |
| TFCC-000 | — | nothing to flag |
Worked examples
Section titled “Worked examples”Raw resource instead of the approved module
Section titled “Raw resource instead of the approved module”diff --git a/infra/rds.tf b/infra/rds.tfnew file mode 100644--- /dev/null+++ b/infra/rds.tf@@ -0,0 +1,4 @@+resource "aws_db_instance" "orders" {+ identifier = "orders-primary"+ storage_encrypted = true+}One unit comes out, action add:
{ "hash": "a1b2c3...", "type": "aws_db_instance", "name": "orders", "action": "add", "body": "+resource \"aws_db_instance\" ...", "metadata": { "generated_at_utc": "...", "source_path": "infra/rds.tf", "group_path": null }}The LLM matches TFCC-001 here: a raw aws_db_instance where the org’s
RDS module was expected.
Going through the approved module instead
Section titled “Going through the approved module instead”diff --git a/infra/rds.tf b/infra/rds.tfnew file mode 100644--- /dev/null+++ b/infra/rds.tf@@ -0,0 +1,5 @@+module "orders_db" {+ source = "git::https://internal.example.com/modules/rds.git?ref=v2.1.0"+ identifier = "orders-primary"+ environment = var.env+}One unit, type: "module", action add — nothing to flag (TFCC-000).
Modifying an existing resource
Section titled “Modifying an existing resource”diff --git a/infra/main.tf b/infra/main.tf--- a/infra/main.tf+++ b/infra/main.tf@@ -1,5 +1,5 @@ resource "aws_db_instance" "orders" { identifier = "orders-primary"- storage_encrypted = false+ storage_encrypted = true tags = {} }One unit, action modify. Because context lines stay in the body alongside
the +/- lines, the LLM can tell this diff enables encryption rather
than disabling it — a fix, not a regression.
A diff touching several files
Section titled “A diff touching several files”Each .tf file’s hunks are processed on their own; anything that isn’t a
.tf file (a README, a CI config, …) never enters the block-matching
step at all.