Skip to content

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.

TerraformCodeChangeParser (opentremor_analyzer_terraform_code_change/parser.py) streams the diff hunk by hunk rather than building the whole diff into memory first:

  1. Walk the diff, yielding one hunk at a time. diff --git a/... b/... headers mark file boundaries; .tf files are kept, anything else is dropped before a single hunk line is even looked at.
  2. 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.
  3. 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 opens withtypename
resource "aws_db_instance" "orders"aws_db_instanceorders
module "rds_postgres"modulerds_postgres
data "aws_ami" "latest"aws_amilatest
variable "env"variableenv
output "db_endpoint"outputdb_endpoint
provider "aws"provideraws
locals / terraformsame as keywordsame as keyword
Markers present in the blockaction
+ onlyadd
- onlydelete
bothmodify

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.

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

Rules live as Markdown in this package’s own opentremor_analyzer_terraform_code_change/rules/ directory:

FileServed at
code-change-rules.mdGET /terraform-code-change/rules
{type}-code-change-rules.mdGET /terraform-code-change/rules?type={type}
Rule IDAreaWhat it catches
TFCC-001Module compliancea raw resource used where an approved module exists
TFCC-002Module compliancemodule source isn’t the approved registry/repo
TFCC-010Resource hygienemissing required tags/labels
TFCC-011Resource hygienehardcoded value instead of a variable
TFCC-020Security regressionthe diff itself weakens security posture
TFCC-021Security regressiona plaintext secret was added
TFCC-030General hygienea Terraform best-practice violation
TFCC-000nothing to flag

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.tf
new 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.

diff --git a/infra/rds.tf b/infra/rds.tf
new 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).

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.

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.