| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | Reading Terraform module outputs when the module block uses count or for_each — single-instance references, count list shape with index and splat, conditional count 0/1 with one(), for_each keyed access, collecting outputs with for expressions, passing outputs between modules, and common unsupported attribute and invalid index errors. Assumes child module outputs are already declared; does not cover writing child output blocks or root output CLI depth. |
| Related guides | Terraform modules |
A single module block exposes outputs as module.NAME.OUTPUT. Add count or for_each on that block and the type of module.NAME changes — which means the dot path you use in the parent must change too.
No meta-arg → module.web.instance_id
count → module.replica[0].instance_id or module.replica[*].instance_id
for_each → module.svc["web"].instance_idThe module inputs and outputs lesson covers how to declare child outputs and wire module.application.application_name. This lesson focuses on what happens when the parent repeats the same child module with count or for_each.
Each scenario uses its own directory under ~/terraform-labs/terraform-module-output-count-for-each/. Create the shared child modules once, then build each demo directory before you run terraform init.
terraform_data only — no cloud provider required.
Shared lab setup
Create the reusable service module and a small consumer module used later in the chain demo.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/modules/service
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/modules/consumer
cd ~/terraform-labs/terraform-module-output-count-for-eachWrite the service child module with instance_id and endpoint outputs:
cat > modules/service/main.tf <<'EOF'
variable "name" {
type = string
}
resource "terraform_data" "this" {
input = var.name
}
output "instance_id" {
value = "svc-${var.name}"
}
output "endpoint" {
value = "${var.name}.example"
}
EOFWrite the consumer module that accepts a producer target_id:
cat > modules/consumer/main.tf <<'EOF'
variable "target_id" {
type = string
}
resource "terraform_data" "consumer" {
input = var.target_id
}
output "consumed_id" {
value = var.target_id
}
EOFEvery demo below references ../../modules/service (or ../../../modules/service from nested directories). The chain demo also uses ../../modules/consumer.
Normal module output reference
With one module instance and no count or for_each, module.NAME is a single object. Read any child output with dot notation.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/single-module
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/single-moduleCreate the root configuration that calls the service module once:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "web" {
source = "../../modules/service"
name = "web"
}
output "web_instance_id" {
value = module.web.instance_id
}
EOFInitialize the single-module demo:
terraform init -input=falsePlan shows the output resolves at plan time because the child output is a known string:
terraform plan -no-color -input=false + module.web.terraform_data.this will be created
Changes to Outputs:
+ web_instance_id = "svc-web"
Plan: 1 to add, 0 to change, 0 to destroy.module.web.instance_id reaches into the child module's output "instance_id" block — the same pattern whether the child holds one resource or many.
How count changes module shape
count on a module block turns module.NAME into a list of module objects, one per index. State addresses use module.replica[0], module.replica[1], and so on.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/count-two
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/count-twoCreate a root configuration with count = 2 and three output access patterns:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "replica" {
source = "../../modules/service"
count = 2
name = "replica-${count.index}"
}
output "first_instance_id" {
value = module.replica[0].instance_id
}
output "all_instance_ids" {
value = module.replica[*].instance_id
}
output "index_to_instance_id" {
value = { for i, m in module.replica : i => m.instance_id }
}
EOFInitialize the count demo:
terraform init -input=falsePlan creates two module instances and previews all three output shapes:
terraform plan -no-color -input=false + module.replica[0].terraform_data.this will be created
+ module.replica[1].terraform_data.this will be created
Changes to Outputs:
+ all_instance_ids = ["svc-replica-0", "svc-replica-1"]
+ first_instance_id = "svc-replica-0"
+ index_to_instance_id = { "0" = "svc-replica-0", "1" = "svc-replica-1" }
Plan: 2 to add, 0 to change, 0 to destroy.module.replica[0] picks one instance. module.replica[*].instance_id splats every instance into a list. The for expression builds a map when you need index keys preserved — useful before passing data to another keyed structure.
Safely read a conditional count module
Feature flags often use count = var.enable ? 1 : 0. When count is zero, module.monitor is an empty list — indexing [0] fails.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/count-conditional
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/count-conditionalCreate the conditional count root with one() on the splat:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "enable" {
type = bool
default = true
}
module "monitor" {
source = "../../modules/service"
count = var.enable ? 1 : 0
name = "monitor"
}
output "monitor_instance_id" {
value = one(module.monitor[*].instance_id)
}
EOFThe splat module.monitor[*].instance_id returns a list with zero or one element. one() extracts the single value when present and returns null when the list is empty.
Initialize the conditional demo with the default enable = true:
terraform init -input=falsePlan shows one module instance and a concrete output value:
terraform plan -no-color -input=false + module.monitor[0].terraform_data.this will be created
Changes to Outputs:
+ monitor_instance_id = "svc-monitor"
Plan: 1 to add, 0 to change, 0 to destroy.Switch to the count-zero variant where enable defaults to false:
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/count-conditional/count-zero
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/count-conditional/count-zeroCopy the same pattern but default enable to false:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "enable" {
type = bool
default = false
}
module "monitor" {
source = "../../../modules/service"
count = var.enable ? 1 : 0
name = "monitor"
}
output "monitor_instance_id" {
value = one(module.monitor[*].instance_id)
}
output "all_monitor_ids_splat" {
value = module.monitor[*].instance_id
}
EOFInitialize and plan with count at zero:
terraform init -input=falsePlan creates no module instances — only output values:
terraform plan -no-color -input=falseChanges to Outputs:
+ all_monitor_ids_splat = []
You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.The splat is an empty list. one(module.monitor[*].instance_id) evaluates to null in the console — safe for optional wiring into another argument that accepts null, unlike module.monitor[0].instance_id which errors on an empty tuple.
How for_each changes module shape
for_each on a module block turns module.NAME into a map keyed by instance key. Bracket syntax uses the for_each key, not a numeric index.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/foreach-map
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/foreach-mapCreate a for_each module root and read one keyed output:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
locals {
services = {
web = "nginx"
api = "orders"
}
}
module "svc" {
source = "../../modules/service"
for_each = local.services
name = each.value
}
output "web_instance_id" {
value = module.svc["web"].instance_id
}
EOFInitialize the for_each demo:
terraform init -input=falsePlan shows keyed module addresses and the selected web output:
terraform plan -no-color -input=false + module.svc["api"].terraform_data.this will be created
+ module.svc["web"].terraform_data.this will be created
Changes to Outputs:
+ web_instance_id = "svc-nginx"
Plan: 2 to add, 0 to change, 0 to destroy.module.svc["web"] selects the instance whose for_each key is "web". The child's name argument came from each.value ("nginx"), which is why instance_id is svc-nginx — the output key and the service name are different concepts.
Collect all module outputs
When you need every instance's output as a list or map, iterate the module object directly. A for_each module is already a map from key to module instance.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/aggregate-outputs
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/aggregate-outputsCreate a for_each root with list and map aggregation outputs:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
locals {
services = {
web = "web"
api = "api"
jobs = "jobs"
}
}
module "svc" {
source = "../../modules/service"
for_each = local.services
name = each.value
}
output "instance_id_list" {
value = [for k, m in module.svc : m.instance_id]
}
output "instance_id_map" {
value = { for k, m in module.svc : k => m.instance_id }
}
output "endpoints_by_key" {
value = { for k, m in module.svc : k => m.endpoint }
}
EOFInitialize the aggregate demo:
terraform init -input=falseApply and read the collected outputs:
terraform apply -auto-approve -input=false -no-colorAfter apply completes, print the aggregated root outputs:
terraform output -no-colorinstance_id_list = [
"svc-api",
"svc-jobs",
"svc-web",
]
instance_id_map = {
"api" = "svc-api"
"jobs" = "svc-jobs"
"web" = "svc-web"
}The list form drops keys; the map form keeps for_each keys attached to each value. Use the map when downstream modules must address the same keys.
Pass one module output to another
When a consumer module repeats with the same keys as a producer module, set for_each = module.svc and read outputs from the producer inside the block.
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/demos/chain-modules
cd ~/terraform-labs/terraform-module-output-count-for-each/demos/chain-modulesCreate a chain root that passes producer outputs into a consumer module:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
locals {
services = {
web = "web"
api = "api"
}
}
module "svc" {
source = "../../modules/service"
for_each = local.services
name = each.value
}
module "consumer" {
source = "../../modules/consumer"
for_each = module.svc
target_id = module.svc[each.key].instance_id
}
output "consumed_by_key" {
value = { for k, m in module.consumer : k => m.consumed_id }
}
EOFfor_each = module.svc gives the consumer module the same instance keys as the producer. module.svc[each.key].instance_id passes the matching output into each consumer instance.
Initialize the chain demo:
terraform init -input=falsePlan wires each consumer to its producer output:
terraform plan -no-color -input=false + module.consumer["api"].terraform_data.consumer will be created
+ module.consumer["web"].terraform_data.consumer will be created
+ module.svc["api"].terraform_data.this will be created
+ module.svc["web"].terraform_data.this will be created
Changes to Outputs:
+ consumed_by_key = {
+ "api" = "svc-api"
+ "web" = "svc-web"
}
Plan: 4 to add, 0 to change, 0 to destroy.Terraform creates an implicit dependency from consumer to producer because the consumer argument references module.svc[each.key].instance_id.
Module output shape quick reference
| Module block | Type of module.NAME |
Read one output | Read all outputs |
|---|---|---|---|
| Single instance | Object | module.web.instance_id |
N/A (one instance) |
count = N |
List of objects | module.replica[0].instance_id |
module.replica[*].instance_id |
count = 0 or 1 |
List (maybe empty) | one(module.monitor[*].instance_id) |
module.monitor[*].instance_id |
for_each |
Map of objects | module.svc["web"].instance_id |
{ for k, m in module.svc : k => m.instance_id } |
Common module output errors
| Symptom | Likely cause | Fix |
|---|---|---|
Unsupported attribute on module.NAME.output |
Module uses count — module.NAME is a list |
Use module.NAME[0].output or splat |
Unsupported attribute — object has attribute "web" |
Module uses for_each — you addressed an output name as if it were a key |
Use module.NAME["web"].output |
| Invalid index — empty tuple | count = 0 and you used [0] |
Use one(module.NAME[*].output) or guard with length() |
| Invalid index — key not in object | Wrong for_each key such as ["missing"] |
Match the key from your for_each map or toset |
| Treating map as list | for_each module indexed with [0] |
Use string keys: module.NAME["key"] |
Unsupported attribute on a counted module
errors/wrong-count-shape/ uses count = 1 but references module.app.instance_id:
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/errors/wrong-count-shape
cd ~/terraform-labs/terraform-module-output-count-for-each/errors/wrong-count-shapeCreate a counted module root that omits the list index:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "app" {
source = "../../modules/service"
count = 1
name = "web"
}
output "bad" {
value = module.app.instance_id
}
EOFInitialize and validate:
terraform init -input=false && terraform validate -no-colorError: Unsupported attribute
on main.tf line 12, in output "bad":
12: value = module.app.instance_id
├────────────────
│ module.app is a list of object
Can't access attributes on a list of objects. Did you mean to access
attribute "instance_id" for a specific element of the list, or across all
elements of the list?Terraform tells you module.app is a list — add [0] or [*].
Unsupported attribute on a for_each module
errors/wrong-foreach-shape/ uses for_each but omits the instance key:
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/errors/wrong-foreach-shape
cd ~/terraform-labs/terraform-module-output-count-for-each/errors/wrong-foreach-shapeCreate a for_each module root that omits the instance key:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "app" {
source = "../../modules/service"
for_each = toset(["a"])
name = each.key
}
output "bad" {
value = module.app.instance_id
}
EOFInitialize and plan:
terraform init -input=false && terraform plan -no-color -input=falseError: Unsupported attribute
on main.tf line 12, in output "bad":
12: value = module.app.instance_id
├────────────────
│ module.app is object with 1 attribute "a"
This object does not have an attribute named "instance_id".module.app is a map whose top-level attributes are instance keys ("a"), not output names. Use module.app["a"].instance_id.
Invalid index when count is zero
errors/count-zero-index/ sets count = 0 and indexes [0]:
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/errors/count-zero-index
cd ~/terraform-labs/terraform-module-output-count-for-each/errors/count-zero-indexCreate a zero-count root that indexes [0]:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "app" {
source = "../../modules/service"
count = 0
name = "web"
}
output "bad" {
value = module.app[0].instance_id
}
EOFInitialize and plan:
terraform init -input=false && terraform plan -no-color -input=falseError: Invalid index
on main.tf line 12, in output "bad":
12: value = module.app[0].instance_id
├────────────────
│ module.app is empty tuple
The given key does not identify an element in this collection value: the
collection has no elements.An empty counted module is a zero-length list — no [0] element exists.
Key mismatch on a for_each module
errors/key-mismatch/ references a key that was never created:
mkdir -p ~/terraform-labs/terraform-module-output-count-for-each/errors/key-mismatch
cd ~/terraform-labs/terraform-module-output-count-for-each/errors/key-mismatchCreate a for_each root that references a nonexistent key:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
module "app" {
source = "../../modules/service"
for_each = toset(["a"])
name = each.key
}
output "bad" {
value = module.app["missing"].instance_id
}
EOFInitialize and plan:
terraform init -input=false && terraform plan -no-color -input=falseError: Invalid index
on main.tf line 12, in output "bad":
12: value = module.app["missing"].instance_id
├────────────────
│ module.app is object with 1 attribute "a"
The given key does not identify an element in this collection value.Only "a" exists in this configuration — verify keys with keys(module.app) in terraform console when wiring unfamiliar modules.
References
- Module blocks — HashiCorp Terraform language docs
- count meta-argument — HashiCorp Terraform language docs
- for_each meta-argument — HashiCorp Terraform language docs
- Splat expressions — HashiCorp Terraform language docs
- one function — HashiCorp Terraform language docs
Summary
You started with the simple case — module.web.instance_id when the module block has no meta-arguments — then walked through how count turns the module into a list and for_each turns it into a keyed map. Indexed access, splats, and for expressions are the three tools you reach for when you need one value, every value, or a reshaped collection.
Conditional count = var.enable ? 1 : 0 is the case that breaks [0] indexing. The splat-plus-one() pattern returns null when no instance exists instead of failing plan. Chaining modules is straightforward when both sides share the same for_each keys — set for_each = module.producer on the consumer and pass module.producer[each.key].output_name into its arguments.
When Terraform reports Unsupported attribute or Invalid index on a module reference, check the module block first: list shape needs brackets, map shape needs string keys, and an empty counted module has no zeroth element. Deeper troubleshooting patterns live in the Unsupported attribute and Invalid index guides.
Frequently Asked Questions
How do I read an output from a module that uses count?
A counted module becomes a list of module objects. Use bracket indexing for one instance, such as module.replica[0].instance_id, or a splat for every instance, such as module.replica[*].instance_id. Do not use module.replica.instance_id without an index.
How do I read an output from a module that uses for_each?
A for_each module becomes a map keyed by instance key. Use string keys in brackets, such as module.svc["web"].instance_id. The key is each.key from the module block, not an output name on the module object.
What is the safe way to read output when count might be zero?
Use a splat plus one(), such as one(module.monitor[*].instance_id), which returns the single value when count is 1 and null when count is 0. Avoid module.monitor[0] unless you know count is always at least 1.
Can I loop over module outputs to build a map?
Yes. When module.svc uses for_each, iterate module.svc with a for expression: { for k, m in module.svc : k => m.instance_id }. The iterator value m is the whole module instance object with all its outputs.

