Terraform

Terraform Output Values: Module Output Design Guide (for Associate)

2026-04-19
NicheeLab Editorial Team

Module outputs are the "public API" you expose to parent modules and other stacks. Which attributes you publish, in what shape, and how much you expose dramatically affect your team's scalability and safety.

This article covers, based on stable specs you should know at the Terraform Associate level, everything from output basics to design patterns, sensitive values, dependencies, and anti-patterns.

Output Basics and Declaration

The output block is the mechanism for exposing values outside a module. A child module's output can be referenced from the parent as module.<name>.<output>, and root module outputs can be retrieved with the terraform output command.

Output types are inferred from the expression. When needed, use tolist, tomap, toset, tostring, etc. to explicitly shape the value. You can specify description, sensitive, and depends_on.

  • Root outputs can be referenced via the CLI and the -json option (sensitive handling discussed below).
  • You cannot reference an output from within the same module. Use locals for internal reuse.
  • Child outputs are referenced in the parent as module.child.output_name (an implicit dependency is created).

Basic output declaration (child to parent)

# modules/storage/outputs.tf
output "bucket_id" {
  description = "S3 bucket ID"
  value       = aws_s3_bucket.this.id
}

# root/main.tf
module "storage" {
  source = "./modules/storage"
}

output "app_bucket_id" {
  value       = module.storage.bucket_id
  description = "App uses this bucket"
}

Designing the Module Output Interface

Design outputs as a "stable interface" — minimal and with future compatibility in mind. Rather than exposing every attribute of underlying resources, the basic approach is to restrict outputs to the key attributes consumers actually need (ID, endpoint, ARN, port, etc.).

Returning multiple related values bundled into an object makes extension easier. Grouping under a logical name like service.endpoint — rather than proliferating individual scalars — avoids name collisions.

  • Limit what you expose to attributes that tend to be stable (e.g. endpoint, ID, port, role ARN).
  • Bundle related values into a single object output, in a shape where future key additions remain backward compatible.
  • Clarify visibility and responsibility, and avoid tight coupling (leakage of internal implementation). Keep sensitive values private by default.
Construct / MeansMain useVisibilityKey caveats
locals (not output)Internal calculation and reuse within a modulePrivate (internal only)Not intended for external use. Keep them easy to test.
Output (scalar)Sharing a single key attribute (ID, ARN)Parent moduleBrittle when attributes grow. Names tend to proliferate.
Output (object)Cohesive publication of related attributesParent moduleKey names form the API contract. Do not break backward compatibility.
data.terraform_remote_stateReading values from another state (cross-stack integration)Other workspaces / statesState dependency creates tight coupling. Organizational standards and governance are essential.

Output flow between modules (stable interface)

module.networkoutputs: vpc_id, subnets[]module.runtimeinputs: vpc_id, subnetsoutputs: service = { id, name, endpoint }rootuses: module.runtime.service.endpointmodule.network → module.runtime → root

Stabilizing the interface with an object output

# modules/runtime/outputs.tf
output "service" {
  description = "Service interface for consumers"
  value = {
    id       = aws_ecs_service.this.id
    name     = aws_ecs_service.this.name
    endpoint = aws_lb.this.dns_name
    port     = 443
  }
}

# root/main.tf (consumer)
module "runtime" {
  source = "./modules/runtime"
  # ...inputs
}

locals {
  service_endpoint = module.runtime.service.endpoint
}

Output Types, Structure, and Shape Stabilization

Output types are inferred from expressions. When you want the shape to stay stable as a contract (API), explicitly coerce with toset/tomap/tolist and build a keyed object (map). Combining can/try is a safe way to prepare for future extensions.

To avoid ambiguity between numbers and strings, use tostring and tonumber to standardize representation and prevent breaking changes on the consumer side.

  • When list order is not meaningful, use toset to convert to a set and reduce diff noise.
  • Keep map keys stable and documented. Avoid unnecessary nesting.
  • Use try with defaults to preserve backward compatibility when new keys are added in the future.

Examples of explicit shaping and backward-compatible coercion

# Explicitly build the shape of the object
locals {
  service_if = {
    id       = aws_ecs_service.this.id
    name     = aws_ecs_service.this.name
    endpoint = aws_lb.this.dns_name
    # Not present yet, but use try as a default for the future
    zone_id  = try(aws_lb.this.zone_id, null)
    ports    = toset([443])
  }
}

output "service" {
  value       = local.service_if
  description = "Stable service interface"
}

# Suppress number/string drift
output "replicas" {
  value = tonumber(var.desired_count)
}

Handling Sensitive Values and Secure Design

Setting sensitive = true on an output, or wrapping the value with sensitive(), hides the value in plan output and terraform output. However, the value is still stored in the state file. Protect it together with remote backend access control and workspace isolation.

Sensitivity propagates downstream. Use nonsensitive() to explicitly clear it when necessary, but evaluate the leakage risk carefully.

  • As a rule, do not output sensitive values. When you absolutely must, consider least-privilege, time-limited credentials and similar measures.
  • terraform output -json returns only metadata with sensitive values hidden, so decide how to handle it in automation integrations during design.
  • Because values remain in state, assume backend, RBAC, and audit controls are in place.

Outputting sensitive values and controlling propagation

# Output the password as sensitive
output "db_password" {
  description = "Generated DB password (avoid exposing if possible)"
  value       = sensitive(random_password.db.result)
  sensitive   = true
}

# Expose only non-sensitive metadata
output "db" {
  value = {
    endpoint = aws_db_instance.this.address
    port     = aws_db_instance.this.port
    engine   = aws_db_instance.this.engine
  }
}

# Clear sensitivity only with great care
output "password_for_debug" {
  value     = nonsensitive(random_password.db.result)
  sensitive = false
  depends_on = [null_resource.allow_debug_window]
}

Dependencies and Evaluation Timing

Outputs are evaluated based on real values at apply time. Unknown values (not yet determined at plan time) are not materialized during the plan phase. When the order in which values are computed matters, you can specify depends_on on outputs as well to make dependencies explicit (Terraform 0.13 and later).

Dependencies between resources in the same module are normally resolved automatically through references. Outputs are strictly an "outward-facing contract" — use locals or explicit references for internal computation and dependency resolution.

  • module.child.output automatically creates a dependency when referenced from the parent.
  • If you need cross-stack integration, consider data.terraform_remote_state. Coupling becomes tighter, however, so design responsibility boundaries and versioning carefully.
  • Outputs cannot be used as arguments to other resources in the same module. Use locals for internal reuse.

Examples of depends_on on outputs and cross-stack references

# Make the output's evaluation order explicit (wait for post-init to finish)
resource "null_resource" "post" {
  triggers = {
    service_id = aws_ecs_service.this.id
  }
  provisioner "local-exec" {
    command = "echo post-init"
  }
}

output "service_endpoint" {
  value      = aws_lb.this.dns_name
  depends_on = [null_resource.post]
}

# Referencing another state (root-side example)
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "tf-states"
    key    = "net/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

module "runtime" {
  source = "./modules/runtime"
  vpc_id  = data.terraform_remote_state.network.outputs.vpc_id
  subnets = data.terraform_remote_state.network.outputs.subnets
}

Versioning and Anti-patterns

Outputs are the module's public API. Changing key names or meanings is a breaking change, so align them with major version bumps. During the transition period, keep the old outputs while adding new ones, and migrate consumers gradually.

Anti-patterns include exposing whole resources, casually exposing sensitive values, and assembling unstable values via string concatenation. Restrict to stable keys, structure them, and avoid leaking internal implementation.

  • Breaking changes go in a major update. Non-breaking changes are minor and limited to adding keys.
  • Mark old outputs as deprecated (note "Deprecated" in the description) and remove them after a grace period.
  • "Expose everything" is a last resort. Check the actual use case and return the bare minimum.

Non-breaking migration example (keep old keys while moving to the new structure)

# New API (preferred)
output "service" {
  description = "Preferred aggregated interface"
  value = {
    id       = aws_ecs_service.this.id
    endpoint = aws_lb.this.dns_name
    port     = 443
  }
}

# Old API (deprecated: for compatibility)
output "service_endpoint" {
  description = "Deprecated: use output 'service.endpoint' instead"
  value       = aws_lb.this.dns_name
}

# Consumers gradually migrate to module.runtime.service.endpoint

Check Your Understanding

Associate

問題 1

In a shared module used by multiple teams, you want to expose an ECS service's ID, endpoint, and port. Fields may be added in the future, and you want to maintain backward compatibility. Which design is the most appropriate?

  1. Publish ID, endpoint, and port as three separate scalar outputs
  2. Publish the entire service resource (aws_ecs_service.this) as a single output
  3. Bundle ID, endpoint, and port into a single object output, in a shape where keys can be added as needed
  4. Have other stacks read resource attributes directly via terraform_remote_state

正解: C

To balance backward compatibility with extensibility, the best approach is to bundle related attributes into a single object output that can be extended non-destructively by adding keys. Splitting into scalars tends to break when keys grow, and exposing the whole resource or referencing it directly via remote_state invites tight coupling.

Frequently Asked Questions

Can I specify a type on an output?

Output blocks do not have a type argument. The type is inferred from the expression. If you want to stabilize the shape, use tolist/tomap/toset to explicitly coerce values, build an object with fixed keys, and document the contract.

If I set sensitive = true, is the value still stored in state?

Yes. The value is hidden in plan output and terraform output, but it is still stored in the state file. Protect it with a remote backend, strict access controls, and auditing.

Do I need to write depends_on on outputs?

Usually not. The implicit dependency on the referenced resource or module is enough. Only add depends_on to outputs in special cases where you need to make the evaluation order explicit (supported in Terraform 0.13 and later).

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.