10.2 Variable Scope within Modules

Key Takeaways

  • The root module is the working directory; a child cannot see root var names unless the parent passes them as module arguments
  • A child's inputs are its own variable blocks; arguments on the calling module block are the only way to assign those inputs
  • Outputs are the only values a parent can read, as module.vpc.vpc_id — never the child's internal resource addresses
  • locals are private to the module that declared them; they are not inherited and are not an export surface
  • Resource addresses are namespaced as module.vpc.aws_vpc.this; there is no implicit inheritance of variables, locals, or resources
Last updated: August 2026

10.2 Variable Scope within Modules

Quick Answer: Each module is a closed namespace. A child cannot see root var.region unless the parent passes region = var.region and the child declares variable "region". Outputs are the only values exported (module.vpc.vpc_id). locals stay private. Resources are addressed as module.vpc.aws_vpc.this. There is no implicit inheritance of variables, locals, or resources.

Objective 5b on Terraform Associate (004) is: describe variable scope within modules. Product version is Terraform 1.12. Official pages: Modules overview, Use modules in your configuration, and the earlier input variable / output language. Section 7 already taught var. and output syntax; this section is the encapsulation rule those features exist to enforce.

Root module versus child module

HashiCorp's wording is consistent: the configuration in the working directory is the root module. Anything you call with a module block is a child module. A child can call further children (nested modules). Terraform still evaluates the whole tree in one plan, but names do not leak across boundaries.

That last sentence is the exam. 004 will show a root variable "environment" and a child resource that reads var.environment with no matching variable block in the child. That configuration is invalid. The child's var. namespace contains only the variable blocks in that child's directory.

# root: variables.tf
variable "environment" {
  type    = string
  default = "prod"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

# root: main.tf — the parent must pass values in
module "vpc" {
  source = "./modules/vpc"

  environment = var.environment
  cidr_block  = var.vpc_cidr
}
# modules/vpc/variables.tf — these names are the child's inputs
variable "environment" {
  type = string
}

variable "cidr_block" {
  type = string
}

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block = var.cidr_block

  tags = {
    Environment = var.environment
  }
}

If you delete environment = var.environment from the module "vpc" block, the child does not quietly pick up the root default. Terraform reports that the child input environment has no value. If you delete variable "environment" from the child but leave the argument on the module block, Terraform reports an unexpected argument. Both sides of the contract are required.

Root assignment sources from chapter 7 — terraform.tfvars, *.auto.tfvars, TF_VAR_, -var, HCP Terraform workspace variables — fill root variable blocks only. They are not a back door into ./modules/vpc.

Inputs are the child's variable blocks

Think of a child's variable blocks as its function parameters. The labels on those blocks are the only argument names the caller may set (aside from meta-arguments such as source, version, count, for_each, providers, and depends_on).

ObjectDeclared whereVisible whereHow a neighbor reads it
InputChild variable "NAME"Only inside that child, as var.NAMEParent assigns NAME = ... on the module block
LocalAny module's locals { }Only inside that same module, as local.NAMEIt does not. Recompute it or export an output
Resource / dataAny moduleOnly inside that same module by its type.nameParent cannot use module.vpc.aws_vpc.this.id
OutputChild output "NAME"Parent reads module.LABEL.NAMEThat is the export surface
Root outputRoot output "NAME"CLI, HCP Terraform UI, terraform_remote_stateterraform output NAME

A required child input is a variable with no default. Optional inputs have defaults. The parent may omit an optional input; it may not omit a required one. The child's validation blocks run against the value the parent passed, not against the root's original variable.

You cannot reach into a child and assign var.cidr_block from a *.tfvars file aimed at the root. There is no module.vpc.var.cidr_block assignment syntax. Pass the argument or change the child's default.

Outputs are the only values exported

A parent that needs the VPC ID cannot write module.vpc.aws_vpc.this.id. That address is valid in state and CLI targeting (terraform state show module.vpc.aws_vpc.this, terraform apply -replace=module.vpc.aws_vpc.this) because Terraform must name every managed object. It is not valid in the parent's HCL expressions. The child must declare an output:

# modules/vpc/outputs.tf
output "vpc_id" {
  description = "ID of the VPC this module manages"
  value       = aws_vpc.this.id
}

output "cidr_block" {
  description = "CIDR assigned to the VPC"
  value       = aws_vpc.this.cidr_block
}
# root
resource "aws_subnet" "app" {
  vpc_id     = module.vpc.vpc_id
  cidr_block = cidrsubnet(module.vpc.cidr_block, 4, 1)
}

The reference shape is module.<LABEL>.<OUTPUT_NAME>. The label is the parent's module "vpc" name, not the directory name and not the child's resource name. If the child does not export vpc_id, the parent cannot invent it.

Child outputs do not appear in terraform output unless the root re-exports them. Encapsulation works in both directions: the CLI lists root outputs, and the parent HCL sees only child outputs.

Locals are module-private

# modules/vpc/main.tf
locals {
  name_prefix = "${var.environment}-net"
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  tags       = merge(local.common_tags, { Name = local.name_prefix })
}

local.name_prefix is invisible to the root. There is no module.vpc.local.name_prefix. If the parent needs that string, the child adds output "name_prefix" { value = local.name_prefix }. The same rule applies in reverse: a child cannot read local.account_alias from the root. Pass it as an input.

004 likes to offer local.vpc.cidr or module.vpc.local.cidr as distractors. Both are fake syntax. Locals never cross a module boundary.

Resource names are namespaced

Two children may both declare resource "aws_vpc" "this". Those objects do not collide. Terraform stores and addresses them as:

  • module.vpc.aws_vpc.this
  • module.network.aws_vpc.this

A nested call becomes module.vpc.module.subnets.aws_subnet.private[0]. Instance keys from count / for_each on the module block sit on the module segment: module.vpc[0].aws_vpc.this or module.vpc["prod"].aws_vpc.this.

That namespacing is why encapsulation is safe. It is also why -replace and moved blocks must use the full module-prefixed address. The parent configuration still cannot read module.vpc.aws_vpc.this.id in an expression; the output remains the public contract.

No implicit inheritance

Memorize the closed list of things that do not flow from parent to child on their own:

  • Root variable values, including defaults
  • Root locals
  • Root resource and data objects
  • Root output values (a child does not read module. of its parent; the parent calls the child, not the other way around)
  • terraform.tfvars / *.auto.tfvars / TF_VAR_ assignments aimed at the root

The one inheritance story Terraform does have is default (unaliased) provider configurations, covered in 10.3. That is a provider rule, not a variable-scope exception. Do not answer a 5b question about var.region with "the child inherits it." It does not.

004 traps for module scope

  • var.NAME inside a child is that child's input, never the root's variable of the same name.
  • Passing environment = var.environment is not optional ceremony when the child needs the value.
  • module.vpc.aws_vpc.this.id is a state address, not a parent expression.
  • local values never leave the declaring module.
  • Two aws_vpc.this resources in different modules are different objects.
  • Root tfvars do not sprinkle into ./modules/*.
Loading diagram...
Variable, local, and output scope across a module boundary
Test Your Knowledge

A root module declares variable "region" { default = "us-east-1" }. The child ./modules/app has no variable "region" block, but a resource in that child uses var.region. What happens on Terraform 1.12?

A
B
C
D
Test Your Knowledge

A child called module "vpc" manages aws_vpc.this and you want the parent to use the VPC ID in an aws_subnet resource. How does the parent read that ID?

A
B
C
D
Test Your Knowledge

Which statement about locals and resource namespacing is true on Terraform 1.12?

A
B
C
D