Terraform Module Inputs and Outputs with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/local 2.6.1 (providers demo)
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user
Scope Module data flow — child input variables, module arguments, variable scope, child outputs, module.NAME.OUTPUT references, chaining outputs into another module input, implicit dependencies, providers meta-argument mapping, brief sensitive propagation, and interface design. Does not cover root variable precedence, tfvars, variable type theory, output CLI depth, provider alias tutorials, Registry discovery, module source or version mechanics, or module publishing.
Related guides Terraform modules
Terraform variables
Terraform output values
Terraform sensitive data
Terraform Associate certification course

The Terraform modules lesson showed how to create a local child module and call it from the root. This lesson answers the next question: how does data move between modules?

text
Root variable
module argument
Child input variable
Child resources
Child output
module.NAME.OUTPUT
Root / another module

You build that chain with two child modules — application and monitoring — under ~/terraform-labs/terraform-module-input-output/. The walkthrough runs in main/; error demos live in sibling errors/ directories.

NOTE
Run terraform init in each working directory before plan. Core examples use terraform_data from the built-in provider. The providers section adds a short hashicorp/local demo in providers-demo/.

How module inputs and outputs connect

A module boundary is an interface. The parent passes inputs as arguments on the module block; the child exposes outputs the parent reads with module.<label>.<output>.

Direction Mechanism Example
Parent → child Module block argument maps to child variable name = var.application_name
Child → parent Child output read by parent module.application.application_name
Child → child One module output wired into another module argument application_name = module.application.application_name

Child modules do not automatically inherit arbitrary Terraform variables from the root. Values must cross the boundary through declared inputs and outputs.


Pass input values to a child module

Create the shared application module under modules/application/. Declare the input in variables.tf:

hcl
variable "name" {
  type = string
}

variable "environment" {
  type    = string
  default = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "The environment value must be dev, staging, or prod."
  }
}

The name variable has no default, so callers must supply it. environment is optional with a Terraform validation checks rule scoped to that variable.

In main/variables.tf, declare a root variable:

hcl
variable "application_name" {
  type    = string
  default = "payments-api"
}

Wire the root into the child in main/main.tf:

hcl
module "application" {
  source = "../modules/application"

  name        = var.application_name
  environment = "prod"
}

var.application_name exists only in the root module namespace. The name = argument is what the child receives as var.name. Without that mapping, the child cannot see the root variable.

Initialize from main/:

bash
cd ~/terraform-labs/terraform-module-input-output/main && terraform init
output
Initializing modules...
- application in ../modules/application
- monitoring in ../modules/monitoring

Terraform has been successfully initialized!

Read outputs from a child module

Expose values from modules/application/outputs.tf:

hcl
output "application_name" {
  value = terraform_data.application.output
}

output "environment" {
  value = var.environment
}

Outputs are the child module's public API. Callers should use these names — not internal resource attributes.

Read the output in the root module:

hcl
output "application_name" {
  value = module.application.application_name
}

Plan to confirm the chain resolves:

bash
terraform plan
output
# module.application.terraform_data.application will be created
  + resource "terraform_data" "application" {
      + input  = "payments-api"
      ...
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + application_name = (known after apply)

Apply the plan, then query the output by name:

bash
terraform apply -auto-approve

The apply transcript ends with application_name = "payments-api". Confirm with the output subcommand:

bash
terraform output application_name
output
"payments-api"

The root printed payments-api because module.application.application_name forwarded the child output after apply.


Pass values between Terraform modules

Chaining modules is where interfaces earn their keep. Add modules/monitoring/ that accepts an application name and stores a monitor record:

hcl
variable "application_name" {
  type = string
}
hcl
resource "terraform_data" "monitor" {
  input = "monitoring:${var.application_name}"
}
hcl
output "monitor_id" {
  value = terraform_data.monitor.id
}

Wire the application output into the monitoring input in main/main.tf:

hcl
module "monitoring" {
  source = "../modules/monitoring"

  application_name = module.application.application_name
}

output "monitor_id" {
  value = module.monitoring.monitor_id
}
text
module.application
       │ output application_name
module.monitoring
       │ input application_name
monitoring:payments-api

That reference creates an implicit dependency between the module instances. Terraform knows that module.monitoring depends on the value exported by module.application and orders the relevant operations accordingly — no explicit depends_on is required.

After apply, verify both modules contributed:

bash
terraform output
output
application_name = "payments-api"
monitor_id = "876c4535-3584-3cb6-39e7-695421ca4ae2"

The monitoring resource received monitoring:payments-api as its input.


Design clear module inputs and outputs

Module quality is mostly interface design. Inputs should be easy to supply; outputs should expose what callers need — nothing more.

Input practice Why
Meaningful names (application_name, not n) Callers understand what to pass without reading main.tf
Type constraints Catch wrong shapes at plan time
description Documents intent in Registry and IDE tooling
Sensible default for optional settings Reduces boilerplate at call sites
validation on the same variable Enforces allowed values at the boundary
Output practice Why
Expose stable identifiers and endpoints Callers compose modules without knowing internal resources
Avoid leaking every resource attribute Implementation can change without breaking parents
Name outputs for consumer tasks application_name beats terraform_data_result

Poor interface:

hcl
# modules/app/outputs.tf — avoid
output "data" {
  value = terraform_data.application
}

Improved interface:

hcl
output "application_name" {
  value = terraform_data.application.output
}

output "environment" {
  value = var.environment
}

The improved version exposes plain strings parents can pass onward. Exporting the whole resource object couples callers to internal structure.


Pass providers to child modules

Provider behavior splits into two ideas:

  • Provider configuration — credentials, region, endpoints, aliases (defined with provider blocks, usually in the root)
  • Provider requirementsrequired_providers version constraints (each reusable child module declares its own)

Child modules inherit default provider configurations from the parent automatically. When the child needs a non-default configuration — typically an alias — pass it with the providers meta-argument. Provider configurations can cross module boundaries; provider source and version requirements are not inherited — each reusable child declares its own required_providers.

The providers-demo/ directory uses hashicorp/local with a secondary alias. Root providers.tf:

hcl
provider "local" {}

provider "local" {
  alias = "secondary"
}

The child module declares what plugin it needs in modules/file-writer/terraform.tf:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.6.1"
    }
  }
}

The child's resource uses the default local provider name inside the module — the parent decides which configuration satisfies it:

hcl
variable "content" {
  type = string
}

resource "local_file" "written" {
  filename = "${path.root}/secondary.txt"
  content  = var.content
}

Map the parent's aliased configuration into the child's default local slot in root main.tf:

hcl
module "via_secondary" {
  source = "./modules/file-writer"

  providers = {
    local = local.secondary
  }

  content = "written through secondary provider alias"
}

The providers map passes configuration from the caller. The child's required_providers block declares requirements. You do not need configuration_aliases here — that meta-argument is for children that expect aliased provider names such as local.secondary inside the module. This mapping connects the parent's local.secondary to the child's ordinary local provider name.

Plan from providers-demo/:

bash
cd ~/terraform-labs/terraform-module-input-output/providers-demo && terraform init

After init, plan to see one file on the default provider and one through the mapped alias:

bash
terraform plan
output
# local_file.root_default will be created
  + resource "local_file" "root_default" { ... }

  # module.via_secondary.local_file.written will be created
  + resource "local_file" "written" { ... }

Plan: 2 to add, 0 to change, 0 to destroy.

The root file uses the default local provider; the child file is created through local.secondary because the parent mapped that configuration into the child's local slot. Full alias patterns live in the Terraform providers lesson.

IMPORTANT
Do not define normal provider configuration blocks inside reusable child modules. Configure providers in the root and pass aliases through providers when needed. Reusable children still declare required_providers for the plugins they need.

Sensitive values across module boundaries

sensitive on a variable or output controls redaction in normal CLI views — it does not remove values from state. Mark a root variable sensitive in main/sensitive.tf:

hcl
variable "api_token" {
  type      = string
  sensitive = true
  default   = "demo-token-not-real"
}

output "api_token_echo" {
  value     = var.api_token
  sensitive = true
}

Plan and apply redact the value in normal listings — look for (sensitive value) under Changes to Outputs during plan, and <sensitive> in the apply summary:

bash
cd ~/terraform-labs/terraform-module-input-output/main && terraform apply -auto-approve
output
Changes to Outputs:
  + api_token_echo = (sensitive value)
...
api_token_echo = <sensitive>
application_name = "payments-api"

sensitive propagates through module arguments when you pass a sensitive value into a child input. For persistence limits, state exposure, and ephemeral alternatives, see Terraform sensitive data.


Common module interface problems

Symptom Likely cause Fix
The argument "name" is required, but no definition was found Missing required module argument Add the argument on the module block: name = var.application_name
An argument named "foo" is not expected here Typo or argument not declared in child Match argument names to child variable blocks; remove unsupported keys
This object does not have an attribute named "does_not_exist" Wrong output name on module.NAME Use an output declared in the child; check spelling
Reference to undeclared input variable inside child Child references var.x never declared in that module Declare variable "x" in the child, then pass x = ... from the parent
Child tries to read root var.application_name directly Variable scope stops at module boundary Pass application_name = var.application_name as a module argument
Provider errors about missing configuration Child needs an alias the root did not pass Add providers = { local = local.secondary } (or the relevant map)
Callers depend on internal resource addresses Over-exposed outputs or direct resource references Publish stable outputs; hide implementation details

Missing required module argument

Remove name from the module "application" block in errors/missing-input/ and plan:

bash
cd ~/terraform-labs/terraform-module-input-output/errors/missing-input && terraform plan
output
Error: Missing required argument

  on main.tf line 1, in module "application":
   1: module "application" {

The argument "name" is required, but no definition was found.

Invalid output reference

Reference a nonexistent output in errors/invalid-output/:

bash
cd ~/terraform-labs/terraform-module-input-output/errors/invalid-output && terraform plan
output
Error: Unsupported attribute

  on main.tf line 9, in output "bad":
   9:   value = module.application.does_not_exist
    ├────────────────
    │ module.application is object with 2 attributes

This object does not have an attribute named "does_not_exist".

Terraform lists how many outputs the module exposes — use one of those names.

When you finish testing in main/, remove lab resources:

bash
cd ~/terraform-labs/terraform-module-input-output/main && terraform destroy -auto-approve
output
Destroy complete! Resources: 2 destroyed.

References


Summary

Module boundaries are contracts. The root passes inputs as module block arguments; each child declares matching variable blocks. Results flow back through output blocks and module.NAME.OUTPUT references. When module.monitoring takes application_name = module.application.application_name, Terraform records an implicit dependency between the module instances and respects it during evaluation and apply — no explicit depends_on required.

Design interfaces deliberately — typed inputs, focused outputs, validation at the boundary. Provider configurations stay in the root; each child still declares required_providers. Pass non-default configurations through providers when a child needs an alias. sensitive redacts normal CLI listings but does not make values disappear from state.

If you have not built a local module yet, start with Terraform modules. Next, learn Registry addresses and version pinning in the module source lesson, or deepen variable and output syntax in the dedicated variables and output chapters when you need root-level detail.


Frequently Asked Questions

1. Do child modules inherit variables from the root module automatically?

No. A child module only sees values passed as arguments on its module block. Map root variables into those arguments explicitly, such as name = var.application_name.

2. How do I read a value from a child module in Terraform?

Use module.NAME.OUTPUT syntax, where NAME is the module block label and OUTPUT is an output block declared inside the child module. Example: module.application.application_name.

3. Does referencing one module output in another module block create a dependency?

Yes. Passing module.application.application_name into module.monitoring creates an implicit dependency. Terraform respects that dependency automatically, so you do not need depends_on for the same relationship.

4. When do I need the providers argument on a module block?

Use providers when a child module must use a specific provider configuration, such as an aliased provider, instead of the default configuration the root passes implicitly. Map configuration aliases in the providers meta-argument.

5. Does sensitive true on a module input hide the value everywhere?

sensitive on a variable or output redacts values in normal plan and apply listings and the default terraform output view. Named output queries and JSON output can still return the value. See the sensitive data lesson for full persistence boundaries.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)