Files
atelier/domains/infrastructure-as-code/modules.md
T
Jon Chery d1aa5daf2b docs(milestone): complete v0.2 — infrastructure-as-code + kubernetes
---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---
2026-08-05 02:20:17 +00:00

4.8 KiB

Modules — Derived Rules

Derives from domains/infrastructure-as-code/first-principles.md. P6 (Modules Compose) lives here. Referenced by terraform.md and opentofu.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.md P1 (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). A README.md is 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