Engineering Standards¶
Standing rules for every contribution to this module. Deviations need
a spec entry justifying why. Same conventions as
phpboyscout/terraform-aws-bootstrap
— if you're cross-referencing across the two repos, anything here is
load-bearing for both.
1. Tagging — non-negotiable¶
Every taggable resource accepts and propagates var.tags. Two-layer
pattern:
- Provider-level
default_tags(set by the caller) — cross-cutting tags likeProject,ManagedBy,Repository. - Module-level
var.tags— exposed by every module, threaded through every taggable resource viamerge(var.tags, { … })for any per-resource additions. Module-supplied tags win on key conflict.
When adding a new taggable resource: it MUST take
tags = merge(var.tags, { Component = "<sub-module>" }) and the
module's variables.tf MUST expose a tags input. No exceptions.
Standard tag set we expect callers to define¶
| Tag | Source | Required? |
|---|---|---|
Project |
provider default_tags |
Yes |
Environment |
provider default_tags |
Yes |
Stack |
provider default_tags |
Yes |
ManagedBy (always opentofu) |
provider default_tags |
Yes |
Repository |
provider default_tags |
Yes |
Component |
per-resource via merge() |
Per-module |
Owner |
provider default_tags (when CODEOWNERS isn't enough) |
Recommended |
CostCenter |
provider default_tags |
Recommended once billing is wired up |
Multi-cloud anticipation¶
The sibling GCP module exposes var.labels (lower-case keys,
alphanumeric + -_, max 63 chars on key and value). The sibling
Azure module exposes var.tags with Azure's stricter value
validation (no <>%&\?/). Each cloud's module is responsible for
its own constraints.
2. Security defaults¶
- No public-facing trust policies by default. OIDC trust
conditions and AssumeRole conditions pin tightly. Operator role
requires
aws:MultiFactorAuthPresent = true. - CMKs over AWS-managed keys. CloudTrail logs and AWS Config history use customer-managed CMKs (separate from each other and from the state-encryption CMK).
- TLS-only bucket policies. Audit and Config buckets refuse non-TLS access at the policy level.
prevent_destroyon irreversible resources. Audit log bucket and Config history bucket — losing those is painful in incident response.- Region restriction by default. The operator role denies
actions outside the caller's
var.allowed_regions(default[var.region]). Carve-outs for genuinely global services.
3. Module input/output discipline¶
- Every
variablehastypeanddescription. Sensitive inputs aresensitive = true. - Every
outputhasdescription. Sensitive outputs aresensitive = true. - No provider configuration inside modules. The caller configures providers.
- No backend configuration inside modules. Modules don't manage their own state.
- Module sources for external dependencies are pinned (commit SHA
or semver tag).
terraform_module_pinned_sourceis enforced via tflint.
4. Naming¶
- Terraform locals:
snake_case, singular. - AWS resource
nameattributes:kebab-case, prefixed with the project tag (pbs-<purpose>). Caller can override via inputs. - Variables:
snake_case. Boolean variables start withenable_/is_/has_. - Outputs:
snake_case, describe the shape (audit_log_bucket_arn, notarn).
5. File organisation within a module¶
main.tf— resource definitions.variables.tf— typed, described inputs.outputs.tf— described outputs.versions.tf—required_version+required_providers.locals.tf— shared locals (optional).data.tf— data sources (optional).README.md— usage example + auto-generated inputs/outputs table from terraform-docs.
Larger modules may split main.tf by concern (main.policy.tf,
main.bucket.tf, etc.).
6. Versioning¶
- Pre-1.0: minor bumps may break the input/output surface; document
in the CHANGELOG with explicit
BREAKING CHANGE:notes. - Post-1.0: semver strictly.
- Tag releases as
v0.1.0,v0.1.1,v1.0.0, etc.
7. Commit style¶
Conventional Commits. Scope is the sub-module short name:
feat(account-hardening):, fix(audit-logging):,
feat(operator-role):. For repo-wide changes use module. For
CI/workflows use ci.
No AI attribution in commit messages.