# 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. ## 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 (`//`) | 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 |