Safeguard
Security Guides•

Why should Terraform module trees stay flat and take inputs?

HashiCorp advises one level of child modules and passing dependencies in as inputs. Why that makes shared infrastructure code easier to review and change.

Safeguard Security Team
4 min read

Keep your module tree to one level of child modules and pass shared things, such as a network, in as input variables instead of letting each module create its own. HashiCorp recommends both, and both make a change easier to read in review.

What the guidance says

HashiCorp's composition page strongly recommends keeping the module tree flat, with only one level of child modules, and connecting modules with expressions in the root module. It describes dependency inversion: pass dependencies in as input variables rather than having a module create its own network or other shared resources. That keeps modules small and lets them coexist with other infrastructure.

On the module development page, the advice is similar. Modules can call other modules, but prefer composition over deeply nested modules, because flatter modules are easier to reuse in different combinations.

Why review gets harder with depth

A pull request that changes a module four levels down shows a small diff. Its effect can be large, because the change travels up through every parent. The reviewer has to trace which callers use it, with what inputs, and what resources will change.

In a flat tree, the root module shows every child and what is passed to it. The reader sees the wiring in one file.

PropertyDeep treeFlat tree
Where wiring livesScattered across levelsRoot module
Blast radius of a leaf changeHard to seeVisible from root
Reuse in other combinationsDifficultEasier
Review effortHighLower

Passing objects, not building them

HashiCorp suggests declaring an object type with only the attributes the module needs. Any object with at least those attributes is accepted, whether it came from a managed resource or a data source.

variable "network" {
  type = object({
    vpc_id     = string
    subnet_ids = list(string)
  })
}

The module does not care how the caller got those values. That makes it possible to point the module at a network owned by another team without changing the module.

Avoid "create if missing" logic

HashiCorp advises against building logic into a module to detect whether something exists and create it if not. The caller should pass in a managed resource if it should be created, or a data source if it already exists. This avoids complex conditional branches and makes intent readable without inspecting remote state.

For security review this is a real gain. A module that decides at plan time whether to create a firewall rule or reuse one is hard to reason about. A module that always receives its dependencies is predictable.

Data-only modules

Modules that only read existing infrastructure through data sources are useful when they hide how the data is retrieved. If the lookup changes, dependent configurations do not need to. HashiCorp also suggests mirroring the outputs of a management module, so you can swap between the two during refactoring.

A reviewer's checklist

  1. Count the levels of module calls. More than one is a prompt for questions.
  2. Look for resources a module creates that other stacks might also need, such as networks and key stores.
  3. Look for count or conditional logic that toggles creation of shared resources.
  4. Check input types are objects with only needed attributes, not whole resource copies.
  5. Confirm the root module shows the wiring clearly.

Related: when a module deserves its own repository.

Where Safeguard fits

Safeguard policies and gates can require reviewers on pull requests that touch shared infrastructure repositories, and evidence automation can record the approvals. The structure advice here is design guidance for your team.

Quick check

Is nesting modules forbidden?

No. HashiCorp says modules can call other modules, but recommends keeping the tree relatively flat.

Who should create the network for a module?

Usually the caller. HashiCorp recommends passing such dependencies in as inputs rather than having each module create its own.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.