| 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 | Module inputs and outputs count vs for_each Terraform modules Unsupported attribute error Invalid index error |
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.

