Stack conventions
Terraform and OpenTofu
HCL infrastructure code (Terraform 1.8+ or OpenTofu 1.8+), including the infra/ stacks Yavaren generates for new applications.
HCL infrastructure code (Terraform 1.8+ or OpenTofu 1.8+), including the infra/ stacks Yavaren generates for new applications.
Added to any stack when the change touches: **/*.tf, **/*.tfvars.
Principles
- Keep the repository's layout and tool (Terraform or OpenTofu); for Yavaren-generated applications that is infra/shared and infra/app, one tfvars and one backend file per environment, applied through ci/iac.sh.
- Run terraform fmt and terraform validate on every change; plan before apply and review the plan, never apply blind.
- Pin required_version and every provider in required_providers with a pessimistic constraint (~>), and commit .terraform.lock.hcl.
- Store state remotely with locking and encryption, one state per stack and environment; never commit state or plan files.
- Never put secret values in code, tfvars or outputs; read them from a secret manager, mark variables and outputs sensitive, and treat state as secret because it can contain them.
- Give every variable a type and a description, add validation blocks for constrained values, and expose only the outputs consumers need.
- Prefer for_each over count for collections so adding or removing an item does not re-create the others.
- Refactor with moved, import and removed blocks rather than manual state surgery.
- Grant least privilege - identities and network rules scoped to what each service uses, with no public endpoints unless required.
- Make environments differ only by variable values, never by copied code.
Conventions
Accessibility
Concurrency
- Rely on state locking (S3 native lockfile with use_lockfile in Terraform/OpenTofu 1.10+, azurerm blob leases, GCS locks) and set -lock-timeout in pipelines.
- Serialise applies per state in CI (one pipeline concurrency group per stack and environment).
Dependencies
- Pin modules from registries to exact versions or narrow ranges, and modules from git to a tag or commit (?ref=v1.2.3).
- Commit .terraform.lock.hcl and update providers deliberately (terraform init -upgrade in a dedicated change).
- Keep Terraform and OpenTofu versions pinned in CI (the generated ci/versions.env does this).
Documentation
- Every variable and output has a description; modules have a README with inputs and outputs (terraform-docs can generate it).
- Comment why a non-obvious setting exists (a quota, a compliance rule), not what HCL already says.
Errors
- Fail early with variable validation and preconditions, with error_message text that tells the operator what to change.
- Use depends_on only for dependencies Terraform cannot infer from references; prefer references.
- Use lifecycle prevent_destroy on stateful resources (databases, key vaults, buckets with data) in production.
Logging
- Enable diagnostic logs and retention for created services (log workspaces, CloudWatch, Cloud Logging) through variables, with retention set per environment.
- Never output secrets; outputs that must carry them are sensitive = true.
Naming
- Use snake_case for resource, data source, variable, output and local names; use lowercase and hyphens in cloud resource names where the provider requires it.
- Do not repeat the resource type in its name (aws_s3_bucket.logs, not aws_s3_bucket.logs_bucket); name a sole instance main or this, consistently.
- Name variables for what they mean with units where relevant (log_retention_days, memory_mb); booleans as enable_x.
Performance
- Split very large states into stacks with separate lifecycles to keep plans fast and blast radius small.
- Avoid data sources that scan large collections on every plan; pass identifiers as variables where possible.
Security
- Keep secrets out of state where the tool allows - generate them in the secret store, pass references, and use ephemeral resources and write-only arguments (Terraform 1.10/1.11+) where providers support them; otherwise restrict and encrypt the state store.
- Encrypt state at rest, restrict who can read it, and enable versioning on the state bucket or container.
- Use workload identity / OIDC federation for the pipeline instead of long-lived cloud keys.
- Default to private networking, encryption at rest and in transit, and least-privilege role assignments per service identity.
- Never disable a scanner finding without a comment explaining the accepted risk.
- Validate every input variable (validation blocks, types, allowed values) so a bad value fails at plan, not in the cloud.
- Never interpolate variables into local-exec or remote-exec commands, or into policy JSON built as strings; use jsonencode and typed arguments (injection).
Structure
- Split a module into conventional files - versions.tf (or terraform.tf), providers, main.tf or files per concern (network.tf, identity.tf, services.tf), variables.tf, outputs.tf, locals and data sources.
- Order inside a block - meta-arguments (count, for_each, provider) first, then arguments, then nested blocks, then lifecycle and depends_on last.
- Keep backend configuration partial in code and pass account-specific values at init (-backend-config), so no account name is committed.
- Keep per-environment values in environments/
.tfvars; keep defaults in variables.tf.
Testing
- Run terraform validate and a plan against every environment's tfvars in CI; treat an unexpected destroy in a plan as a failure to investigate.
- Use terraform test (.tftest.hcl, Terraform 1.6+ and OpenTofu) for modules with logic - variable validation, for_each shaping, naming.
- Lint with tflint (with the cloud's ruleset) and scan with a misconfiguration scanner (Trivy, Checkov) in CI.
- Unit-test module logic with
terraform testplan-mode runs and mocked providers (mock_provider) where the tool supports them; integration-test with apply-mode runs in a sandbox account only. - Name test files after the behaviour they cover (
tests/naming.tftest.hcl) and each run block after its case. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Is the code terraform fmt clean and does terraform validate pass for every root module?
- Are required_version and all providers pinned with ~> constraints, and is .terraform.lock.hcl committed and updated?
- Is state remote, locked and encrypted, with no state, plan or crash files committed?
- Are secrets kept out of code, tfvars and outputs, and are sensitive variables and outputs marked sensitive?
- Does every new variable have a type, description and, where constrained, a validation block?
- Are collections created with for_each keyed by stable names rather than count?
- Are renames and moves expressed with moved blocks so the plan shows no destroy and re-create?
- Do environments differ only by tfvars values?
- Are identities, role assignments and network rules least privilege, with no unintended public exposure?
- Do stateful production resources have deletion protection (prevent_destroy or the provider's own setting)?
- Are tags and naming applied consistently through locals or provider defaults?
- Does tflint and the misconfiguration scanner report no new high or critical findings?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
terraform-fmt |
format | terraform fmt | no |
terraform-validate |
static | terraform validate | no |
tflint |
lint | tflint | no |
trivy-config |
security | trivy | no |
checkov |
security | checkov | no |
terraform-validate-all |
static | terraform validate | no |
terraform-test |
test | terraform test | no |
gitleaks |
security | gitleaks | no |