d1aa5daf2b
---ci--- project: atelier phase: 5 milestone: v0.2 status: complete requirements: covered: [ATELIER-36, ATELIER-37, ATELIER-38, ATELIER-39, ATELIER-40, ATELIER-41, ATELIER-42, ATELIER-43, ATELIER-44, ATELIER-45, ATELIER-46, ATELIER-47, ATELIER-48, ATELIER-49, ATELIER-50, ATELIER-51, ATELIER-52, ATELIER-53, ATELIER-54, ATELIER-55, ATELIER-56, ATELIER-57, ATELIER-58, ATELIER-59] partial: [] ---/ci---
4.8 KiB
4.8 KiB
Modules — Derived Rules
Derives from
domains/infrastructure-as-code/first-principles.md. P6 (Modules Compose) lives here. Referenced byterraform.mdandopentofu.md.
Why Modules (P6 Modules Compose)
- A module is the unit of reuse, review, and versioning in IaC. It encapsulates a repeatable pattern behind a typed interface.
- Composition — building large from small — is the IaC expression of core C6 Composability. Without modules, every stack is a one-off; with modules, a stack is an assembly of reviewed parts.
- A good module has one job (a VPC, a database, a load balancer), a small typed surface, and no hidden side effects.
- A versioned module is the IaC expression of
domains/devops/first-principles.mdP1 (Reproducibility) and P6 (Configuration as Code): a module pins a reusable, rebuildable pattern that any environment can call.
Module Structure (P1 Declarative Intent, C2 Clarity)
- The conventional layout:
main.tf(resources),variables.tf(inputs),outputs.tf(outputs),versions.tf(provider/version pins). AREADME.mdis required for any published module. - Inputs are typed and validated:
variable "name" { type = string, description = "...", validation { ... } }. The description is the contract. - Outputs are the module's interface to consumers. Mark sensitive outputs
sensitive = true. Document non-obvious outputs in the description. - A module does not declare a provider configuration unless it owns the provider. Most modules declare only
required_providers(the constraint) and let the consumer configure the provider.
Versioning (P5 Version Everything)
- Modules are versioned. The registry expects SemVer tags (
v1.0.0). A consumer pins to a version or a range (~> 1.0). - A breaking change bumps the major. An additive change bumps the minor. A fix bumps the patch. No silent breaking changes within a minor.
- Tag the module repo; the tag IS the version. Never
source = "git::...?ref=main"in prod — unversioned modules drift.
Source Patterns (P5 Version Everything)
| Source | When | Risk |
|---|---|---|
Registry (<ns>/<name>/<provider>) |
Public, versioned, signed | Verify the publisher; pin the version |
Git (git::https://...?ref=v1.0.0) |
Private modules across repos | Pin to a tag, not a branch |
Local (./modules/networking) |
Monorepo, single repo | Re-reviewed on every change; no independent version |
| Inline (no module) | Trivial one-off | Becomes a copy-paste anti-pattern at scale |
- Local modules in a monorepo are fine — they trade independent versioning for co-evolution. The boundary is the review unit: if the module and the consumer always change together, local is correct.
- Cross-repo modules must be versioned via git tags; unversioned cross-repo modules are the worst case (drift without a version to pin).
The Module-vs-Copy Boundary (P6 Modules Compose)
- If a block is used more than once, it is a module. If it is used once and will never be reused, inline is acceptable.
- If two copies differ in one attribute, that is a module with a variable, not two copies. The variable is the difference; the shared body is the module.
- If you find yourself copy-pasting a block and editing it, stop. The edit is a variable. The copy is a module call.
- A module that has grown to do many jobs should be split. A module with 20 variables is two modules.
Composition (P6 Modules Compose, C6 Composability)
- Compose by calling modules from a root configuration:
module "vpc" { source = "...", version = "..." }. The root is the assembly; the modules are the parts. - Outputs of one module feed inputs of another:
module "app" { vpc_id = module.vpc.vpc_id }. This is the composition edge. - Avoid hidden coupling: a module should not reach into another module's state. If two modules must share state, promote the shared concern to the root or a parent module.
Reviewing Modules (P4 Plan Before Apply)
- A module is reviewed once, at its version. Consumers trust the version pin. A module change requires a new version and a review of the diff.
- When a module changes, every consumer that bumps the version gets the change. Treat a module version bump as a real change: review the module diff, run the consumer's
plan. - A module with a breaking change must not auto-bump in consumers. Pin consumers to the old major until they explicitly migrate.
What Violates Module Discipline
| Violation | Principle |
|---|---|
| Copy-pasted block with a one-line difference | P6 Modules Compose |
source = "git::...?ref=main" in prod |
P5 Version Everything |
| Module with 20 variables | P6 Modules Compose (split it) |
| Silent breaking change within a minor | P5 Version Everything |
| Module reaching into another module's state | C6 Composability, P1 Declarative Intent |
| Unpublished module with no README | C2 Clarity |