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.
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.
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"
}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.
| Construct / Means | Main use | Visibility | Key caveats |
|---|---|---|---|
| locals (not output) | Internal calculation and reuse within a module | Private (internal only) | Not intended for external use. Keep them easy to test. |
| Output (scalar) | Sharing a single key attribute (ID, ARN) | Parent module | Brittle when attributes grow. Names tend to proliferate. |
| Output (object) | Cohesive publication of related attributes | Parent module | Key names form the API contract. Do not break backward compatibility. |
| data.terraform_remote_state | Reading values from another state (cross-stack integration) | Other workspaces / states | State dependency creates tight coupling. Organizational standards and governance are essential. |
Output flow between modules (stable interface)
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 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.
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)
}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.
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]
}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.
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
}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.
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.endpointAssociate
問題 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?
正解: 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.
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).
Practice with certification-focused question sets
無料で問題を解いてみる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.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...