3 Review principles
Good Terraform review is guided by a small set of principles. These apply regardless of what the code is doing — whether it creates a storage bucket or defines a complex network topology. Keep them in mind as you read through a pull request.
3.1 Correctness
The code should do what the pull request says it does. This sounds obvious, but it is easy to miss a mismatch between the description (“adds read-only IAM binding for service account X”) and the actual resource definition (which grants a broader role, or targets the wrong project).
Check:
- Does the resource configuration match the intent described in the PR?
- Are resource names, labels, and references consistent throughout?
- Are there any typos in resource names or identifiers that Terraform would accept but would create the wrong resource?
3.2 Least privilege
Infrastructure should grant only the permissions needed to do the job — nothing more. This applies to IAM roles, network firewall rules, service account scopes, and any other access control mechanism.
Check:
- Does each IAM binding use the most specific role available rather than a broad primitive role such as
roles/editororroles/owner? - Are firewall rules restricted to the ports and source ranges actually needed?
- Does the service account used by a workload have only the permissions that workload requires?
Primitive IAM roles (roles/owner, roles/editor, roles/viewer) grant access across an entire project. They should almost never appear in production Terraform code. If you see one, ask why a more specific role cannot be used.
GCP-specific: In Google Cloud, prefer predefined roles (e.g. roles/storage.objectViewer) or custom roles over primitive roles. Where a resource supports it, consider using resource-level IAM bindings rather than project-level bindings.
3.3 Idempotency
Running terraform apply twice in a row with no code changes should produce no changes on the second run. Code that is not idempotent — where the same apply produces different results each time — is fragile and hard to debug.
In practice, idempotency issues often arise from:
- Resources that generate random values outside Terraform’s knowledge
- External data sources that return different values on each run
local-execorremote-execprovisioners that run shell commands with side effects
Check:
- Does the code use
random_*resources correctly, with alifecycleblock to prevent unnecessary recreation? - Are external data sources used appropriately, and are their results stable?
- Are provisioners (if present at all) genuinely necessary and side-effect safe?
3.4 Explicitness
Terraform allows many defaults. Relying on defaults makes code harder to review and harder to reason about when defaults change in a future provider version.
Check:
- Are important settings declared explicitly rather than relying on provider or resource defaults?
- Are
required_versionandrequired_providersversion constraints set? - Are provider versions pinned to a specific version or a tight range, rather than accepting any version?
A terraform block with required_version and required_providers at the top of each root module makes the code’s dependencies clear and prevents unexpected behaviour when provider versions change.
3.5 Minimal blast radius
A change should affect only what it needs to affect. Infrastructure changes that touch many resources at once are harder to review, harder to test, and more likely to cause an outage if something goes wrong.
Check:
- Is the scope of this change appropriate? Could it be broken into smaller, safer PRs?
- Does the change target only the intended environment directories (e.g.
01_sandbox/) or does it affect shared modules used by all environments? - Are there any resources marked for replacement (
-/+in the plan) that are not expected to be recreated?
3.6 Auditability
Infrastructure changes should be traceable. Anyone looking at the state, the plan, or the deployed resources six months from now should be able to understand what exists, why it exists, and who is responsible for it.
Check:
- Are resources tagged or labelled consistently? At minimum, resources should carry labels that identify the project, environment, and owning team.
- Is there a clear description or comment for any non-obvious configuration choice?
- Does the module or directory have up-to-date documentation?
GCP-specific: Google Cloud supports resource labels at the project and resource level. A labelling convention (e.g. env, team, service) makes cost attribution, access reviews, and incident response significantly easier.
3.7 No secrets in code
Secrets — passwords, API keys, tokens, private keys — must never appear in Terraform code or state files committed to version control.
Check:
- Are sensitive values passed in via variables marked
sensitive = true, rather than hardcoded? - Are secrets retrieved from a secrets manager (such as Secret Manager on GCP) rather than stored in
.tfvarsfiles that could be committed? - Does the
.gitignoreexclude*.tfvars,terraform.tfstate, andterraform.tfstate.backup?
If you find a hardcoded secret in a pull request, block the PR and treat it as a potential secret leak. Even if the PR is not merged, the secret may already be visible in the branch history. Follow your organisation’s incident response process.