Skip to main content
Let’s clarify how variable scope works in Terraform modules. This is a common stumbling block for newcomers, but once you understand the rules, modules behave predictably — and it’s a topic often covered on the Terraform Associate Certification: HashiCorp Certified exam. This guide covers:
  • What “module scope” means
  • How to pass values into child modules
  • How to return values from child modules to the root
  • Practical examples and best practices

Project layout (overview)

Typical structure:
  • Root (parent) module
    • main.tf — module blocks and resources
    • variables.tf — input variables for the root
    • outputs.tf — values exposed by the root
  • Child module(s)
    • Their own main.tf, variables.tf, and outputs.tf — logically isolated modules (e.g., networking, compute)

Root module variables (example)

Define variables in the root module’s variables.tf:

Child module variables (example)

A child module declares its own inputs in its variables.tf:
Key concept: independent scopes
  • Each module (root and child) has its own scope. Variables defined in the root (for example environment) do not automatically become available inside a child module (which might expect env).
  • Likewise, variables and resources inside a child module are not visible to the root unless the child explicitly exposes them via outputs.
  • Think of modules like functions: they only receive what you explicitly pass in and return what you explicitly output.
A purple infographic titled "Understanding Variable Isolation" showing a Root Module and a Child Module separated by a boundary with red X marks indicating variables and resources can't cross between modules. A caption at the bottom states that each module has its own isolated variable scope.

Passing data into a child module (module block)

To feed values into a child module, declare the module block in the root module and assign arguments whose names match the child module’s variable names. Example: pass the root environment into the child’s env:
Inside the child module you reference that value as var.env in resource blocks. You can pass:
  • a root variable (var.*)
  • a resource attribute (aws_subnet.my_subnet.id)
  • another module’s output (module.network.subnet_id)
  • a literal/hard-coded value ("prod")
Example passing a resource attribute:

Real-world example: calling a community VPC module

When consuming a module from the registry, pass in the expected inputs (names match the child module’s variables):
Notes:
  • The child module defines the input variables (e.g., cidr, azs) in its own variables.tf.
  • If the child sets defaults for some variables, those defaults apply when you omit the corresponding argument in the module block.

Outputs: sending data from child back to root

To expose values from a child module back to the root, the child must declare outputs in its outputs.tf. The root can then access those values as module.<name>.<output_name>. Child module example outputs.tf:
Root module usage examples:

Common mistakes and how to avoid them

Best practices and checklist

  • Treat modules as isolated units: nothing is shared implicitly.
  • Always pass inputs explicitly via module block arguments.
  • Expose only the necessary values using outputs in child modules.
  • Use default values for non-critical settings to reduce boilerplate.
  • Require explicit inputs for environment-specific values (names, CIDRs, accounts).
  • Document module inputs and outputs in a README for each module.
Quick checklist:
  • Does the child module declare a variable for each input you need?
  • Are you passing values explicitly in the module block?
  • Are any required values missing (no defaults)?
  • Has the child module exposed required IDs/addresses via outputs?
Modules are isolated by design. Always pass values explicitly (via module inputs) and expose values via outputs — this makes modules reusable and predictable.

Watch Video