Study guide
10.2 Variable Scope within Modules
HashiCorp Certified: Terraform Associate 004
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan
2 Infrastructure as Code with Terraform
2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows
3 Terraform Fundamentals: Providers & State
3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State
4 Core Terraform Workflow: Init & Validate
4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration
5 Plan, Apply, Destroy, and Formatting
5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments
6 Resources, Data Sources, and References
6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns
7 Variables, Outputs, and Complex Types
7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set
8 Dynamic Configuration and Dependencies
8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules
9 Conditions, Secrets, and Sensitive Data
9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments
10 Terraform Modules
10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions
11 Terraform State Management
11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations
12 Maintain Infrastructure with Terraform
12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics
13 HCP Terraform
13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations
10.2 Variable Scope within Modules
Key Takeaways Explain This Topic AI
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 ).
- Object Declared where Visible where How a neighbor reads it
- Input Child variable "NAME" Only inside that child, as var.NAME Parent assigns NAME = ... on the module block
Local Any module's locals { } Only inside that same module, as local.NAME It does not. Recompute it or export an output
Resource / data Any module Only inside that same module by its type.name Parent cannot use module.vpc.aws_vpc.this.id
- Output Child output "NAME" Parent reads module.LABEL.NAME That is the export surface
- Root output Root output "NAME" CLI, HCP Terraform UI, terraform_remote_state terraform 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/* .