Chapter 6: Modules
Topic 40

Composition and Nested Modules

CompositionModules

Real systems are built by composing modules — a root module calling a network module, a compute module, and a data module, wiring one's outputs into another's inputs. The art is keeping the composition shallow and the data flow one-directional. Modules calling modules calling modules become impossible to reason about or debug well before they technically break.

Composition is where modules stop being a convenience and become an architecture. Done well, the root reads like a wiring diagram of the system. Done badly, a single change means opening five directories to trace where a value comes from, and a dependency cycle appears at apply time with no obvious cause.

A shallow composition
Root module
holds the system-specific wiring
network
VPC · subnets · routes
its subnet IDs flow one-directionally into compute
compute
instances · load balancer
data
RDS · backups

The Composition Pattern

The root module is the wiring layer. It calls a handful of focused child modules and connects them by passing the outputs of one as the inputs of the next — the network module's subnet IDs become the compute module's input. The children do not know about each other; the root holds the knowledge of how they fit together. That keeps each module reusable in isolation and confines the system-specific wiring to one place.

A root module wiring two children together
module "network" {
  source     = "./modules/vpc"
  cidr_block = "10.0.0.0/16"
  az_count   = 3
}

module "compute" {
  source        = "./modules/asg"
  # one module's output becomes another's input
  subnet_ids    = module.network.private_subnet_ids
  instance_type = "t3.medium"
}

Flat vs Deeply Nested

A child module can call its own children, and a little of that is fine. The failure mode is depth: a root that calls a module that calls a module that calls a module, three or four levels down. To change one value you open every level to trace it, and an output at the bottom has to be re-exported through every layer above to reach the top. Most teams favor a shallow tree — a root composing a small number of single-purpose modules — over a deep one, because shallow stays legible.

Data Flow Between Modules

Wire modules with the specific values each needs, and keep the flow in one direction. module.network.private_subnet_ids feeds module.compute, which produces an autoscaling group name that feeds module.monitoring — a clean one-way chain. Passing a whole undifferentiated object between modules instead of the two values one actually needs couples them tightly: the consumer now depends on the entire shape of the producer, and a change anywhere in it ripples through.

The Wrapper Module Pattern

A wrapper is a thin internal module that wraps a third-party one to standardize its use. Instead of every team calling the community VPC module directly with its full surface, they call your modules/network wrapper, which sets your tags and naming, pins the upstream version, and exposes only the inputs your org allows. It is one level of nesting that earns its keep — a single point to upgrade and enforce standards — as opposed to the gratuitous depth of modules nested for no reason.

Avoiding Hidden Cycles

Module wiring can create a dependency cycle that is invisible in any single file. If module A takes an input from module B and module B takes an input from module A, Terraform sees a loop and fails — but because the edges are spread across two module blocks, the cause is not obvious from either one alone. Designing module interfaces so data flows one direction prevents this; when a cycle does appear, the fix is to break the bidirectional wiring, often by moving a value up to the root, not to paper over it with depends_on.

Common Mistakes
  • Nesting modules three or four levels deep, so a single change means opening five directories to trace the data flow and re-exporting outputs through every layer.
  • Wiring module outputs back and forth until a dependency cycle appears at apply time, with the two edges spread across separate blocks and no obvious cause.
  • Passing a huge undifferentiated object between modules instead of the specific values each needs, coupling them so any change to the producer ripples through.
  • Building a speculative composition layer before there is a second consumer to justify it, adding indirection that serves no real reuse.
  • Breaking a cycle with depends_on instead of fixing the bidirectional wiring, masking the design problem rather than removing it.
Best Practices
  • Keep the module tree shallow: a root that composes a small number of focused, single-purpose modules rather than deep chains.
  • Wire modules with specific output-to-input connections, keeping the data flow one-directional and acyclic.
  • Pass the exact values a module needs, not a whole object, so consumers do not depend on a producer's full shape.
  • Use a wrapper module to standardize a third-party module — one deliberate level of nesting — instead of deep gratuitous nesting.
  • Let real reuse drive when you add a composition layer, and break any cycle by reshaping the data flow, not with depends_on.
Comparable tools CloudFormation nested stacks compose similarly — and share the deep-nesting pain Pulumi composes component resources in code Ansible composes roles, with the same flat-is-better discipline

Knowledge Check

What is the root module's role in composition?

  • It is the wiring layer — it calls focused child modules and connects them by passing one's outputs as another's inputs
  • It holds no resources of its own and merely stores the shared state file on behalf of all of the child modules it calls
  • It forces every child module to share one single provider configuration with no aliases
  • It exposes each child's internal resources to every other child module automatically

What is the trade-off of deeply nested modules versus a shallow tree?

  • Deep nesting hides the data flow — a change means opening every level and re-exporting outputs up the chain — while a shallow tree reads clearly
  • Deep nesting is faster to apply because Terraform parallelizes the graph by module depth rather than by resource dependencies, so each extra level adds concurrency
  • Terraform refuses to plan a configuration where modules are nested more than two levels deep
  • A shallow tree cannot share outputs between its sibling child modules

How can module wiring create a hidden dependency cycle?

  • Module A takes an input from B while B takes an input from A, so the loop is spread across two blocks and invisible in either alone
  • Two modules that are passed the same provider configuration will always form a cycle
  • A module that declares more outputs than it declares inputs creates a dependency cycle, since the extra exported values feed back on themselves
  • Cycles only happen when a single module recursively calls itself in its own source

Why is the wrapper module pattern worth its one level of nesting?

  • It gives a single point to set standards, pin the upstream version, and constrain the interface for every caller
  • It removes the need to pin the wrapped upstream module to any specific version
  • It lets callers reach the wrapped module's internal resources and read their attributes directly without outputs
  • It converts a Registry module into a local-path source to make applies run faster

You got correct