Terraform Module Architecture

This guide covers designing reusable Terraform modules, composition layers, and the deep hierarchy model for platform engineering at scale.

Module Roles

Role Responsibility
Primitive module Wraps one resource family with strict interface
Composite module Assembles multiple primitives for a deployable capability
Root composition Injects environment values and wiring only

Keep business policy out of primitives when it is environment-specific.

Contract Design

A good module contract has:

  • Strongly typed inputs
  • Defaults only for safe/common behavior
  • Explicit outputs for consumers
  • Preconditions for invariants

Bad contract smells:

  • Many loosely typed maps
  • Opaque passthrough variables
  • Outputs that mirror entire provider objects

Suggested File Layout

File Purpose
main.tf Resources and module calls
variables.tf Typed input contract and validation
outputs.tf Explicit consumer interface
versions.tf Runtime and provider constraints
locals.tf Computed values, naming, shared labels

Composition Rules

  • Pass only required values into child modules
  • Avoid circular dependencies and hidden ordering
  • Prefer data flow via input/output over broad depends_on
  • Keep module count manageable; over-fragmentation hurts maintainability

Deep Hierarchy Model

For platform engineering at scale, use a 5-level module hierarchy:

L4: Org Orchestration
  └── L3: Environment Roots
        └── L2: Domain Stacks
              └── L1: Composites
                    └── L0: Primitives
Level Role Examples
L0 Primitives One resource family, strict contract VPC, IAM role, S3 bucket
L1 Composites Capability units built from primitives Networking stack, compute cluster
L2 Domain stacks Bounded business domains Payments, identity, observability
L3 Environment roots Env-specific wiring and configuration dev, staging, production
L4 Org orchestration Account/project vending and shared policy Organization policies, account factory

Composition Rules

  • Dependencies flow downward only (L4 -> L3 -> L2 -> L1 -> L0)
  • No lateral imports across the same level without an explicit interface contract
  • Cross-state data flow is via explicit outputs or approved remote state access
  • Each level owns its state boundary and apply lifecycle
  • Environment roots should not embed business logic; keep it in L2/L1

Decision Aid

Add a new level only if ownership, lifecycle, or blast radius requires it.

Module Release Discipline

  • Tag module versions
  • Use bounded version constraints in consumers
  • Run compatibility tests before raising lower bounds

When to Create a New Module

Create a new module only when at least one is true:

  • Reused across 2+ stacks
  • Ownership differs from current module
  • Lifecycle differs significantly
  • Change blast radius needs isolation

results matching ""

    No results matching ""