Terraform / HCL

Infrastructure as code.

What this is

Infrastructure as code. The BLOCKER tier is almost entirely things that are expensive or irreversible to get wrong in a plan: secrets committed in .tf/.tfvars, public exposure without a justification comment, a resource renamed without a moved {} block (plan shows destroy+create on live data), IAM scoped too broadly, unpinned provider/module sources. The HIGH tier is mostly configuration hygiene that prevents surprise diffs and gaps: count where for_each is correct, missing backup retention or encryption on new stateful resources.

How to onboard it

A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:

terminal
$ npx redlinegate init  # detects the profile
$ npx redlinegate init --profile infra  # or name one

One profile pulls these rules in: infra.

Once onboarded, your files match this stack when they fit any of these globs:

  • **/*.tf
  • **/*.tfvars
  • **/*.hcl

How to use it — 17 rules

Nothing to run. Once your profile includes Terraform / HCL, redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.

Rule idSeverityCatches
terraform/secrets-in-configBLOCKERSecrets in `.tf`/`.tfvars` — passwords, keys, tokens, connection strings. Reference Secrets Manager / SSM / vault data sources; mark variables `sensitive = true`.
terraform/public-exposureBLOCKERPublic exposure: `0.0.0.0/0` ingress on non-public ports, public S3 bucket ACLs/policies, `publicly_accessible = true` on databases — without an explicit justification comment and approval.
terraform/missing-moved-blockBLOCKERResource rename/move without `moved {}` block — plan shows destroy+create; data loss on stateful resources.
terraform/overly-broad-iamBLOCKEROverly broad IAM: `Action: "*"`, `Resource: "*"`, or `iam:PassRole` unscoped — least privilege, scoped ARNs.
terraform/prevent-destroy-removedBLOCKER`prevent_destroy` removed or lifecycle guards deleted on stateful resources (DBs, buckets, tables) without stated intent.
terraform/unpinned-sourceBLOCKERProvider or module source unpinned — exact version or bounded constraint (`~>`), never floating latest.
terraform/count-vs-for-eachHIGH`count` on identity-bearing resources where `for_each` is correct — index shifts destroy/recreate siblings.
terraform/hardcoded-env-valuesHIGHCross-environment values hardcoded in modules (account IDs, ARNs, CIDRs) — pass as variables.
terraform/missing-required-tagsHIGHMissing required tags on new resources (owner, cost-centre, environment — per org tagging policy).
terraform/wide-open-egressHIGHWide-open egress added without comment.
terraform/ambiguous-data-sourceHIGHData sources fetching by name/tag that may match multiple resources — brittle; use IDs where stable.
terraform/missing-backup-retentionHIGHNew stateful resource without backup/retention configuration (RDS retention, S3 versioning, DynamoDB PITR).
terraform/missing-encryptionHIGHEncryption not explicit on new data stores (at-rest KMS, in-transit enforced).
terraform/variable-metadataSUGGESTIONVariables missing `description` and `type`.
terraform/output-metadataSUGGESTIONOutputs missing `description`; sensitive outputs not marked `sensitive`.
terraform/prefer-jsonencodeSUGGESTIONInline JSON policies over `jsonencode()`/data-source policy documents.
terraform/repeated-literalSUGGESTIONRepeated literal in 3+ places — promote to local.

Expected output

A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:

a finding from this file
Redline/BLOCKER [terraform/secrets-in-config]: <one-line problem>

Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 17 rules here, 6 BLOCKER, 7 HIGH and 4 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.

If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.

How to edit it

Rules are edited in standards/stacks/terraform.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.

  1. Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
  2. node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
  3. node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
  4. Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
  5. Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
  6. node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.

Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.

The full file

standards/stacks/terraform.md · 28 lines · 2.4 KB
# Terraform / HCL Review Rules

## BLOCKER — request changes

- `terraform/secrets-in-config` — **Secrets in `.tf`/`.tfvars`** — passwords, keys, tokens, connection strings. Reference Secrets Manager / SSM / vault data sources; mark variables `sensitive = true`.
- `terraform/public-exposure` — **Public exposure**: `0.0.0.0/0` ingress on non-public ports, public S3 bucket ACLs/policies, `publicly_accessible = true` on databases — without an explicit justification comment and approval.
- `terraform/missing-moved-block` — **Resource rename/move without `moved {}` block** — plan shows destroy+create; data loss on stateful resources.
- `terraform/overly-broad-iam` — **Overly broad IAM**: `Action: "*"`, `Resource: "*"`, or `iam:PassRole` unscoped — least privilege, scoped ARNs.
- `terraform/prevent-destroy-removed` — **`prevent_destroy` removed** or lifecycle guards deleted on stateful resources (DBs, buckets, tables) without stated intent.
- `terraform/unpinned-source` — **Provider or module source unpinned** — exact version or bounded constraint (`~>`), never floating latest.

## HIGH

- `terraform/count-vs-for-each` — `count` on identity-bearing resources where `for_each` is correct — index shifts destroy/recreate siblings.
- `terraform/hardcoded-env-values` — Cross-environment values hardcoded in modules (account IDs, ARNs, CIDRs) — pass as variables.
- `terraform/missing-required-tags` — Missing required tags on new resources (owner, cost-centre, environment — per org tagging policy).
- `terraform/wide-open-egress` — Wide-open egress added without comment.
- `terraform/ambiguous-data-source` — Data sources fetching by name/tag that may match multiple resources — brittle; use IDs where stable.
- `terraform/missing-backup-retention` — New stateful resource without backup/retention configuration (RDS retention, S3 versioning, DynamoDB PITR).
- `terraform/missing-encryption` — Encryption not explicit on new data stores (at-rest KMS, in-transit enforced).

## SUGGESTION

- `terraform/variable-metadata` — Variables missing `description` and `type`.
- `terraform/output-metadata` — Outputs missing `description`; sensitive outputs not marked `sensitive`.
- `terraform/prefer-jsonencode` — Inline JSON policies over `jsonencode()`/data-source policy documents.
- `terraform/repeated-literal` — Repeated literal in 3+ places — promote to local.