skip to content

In Terraform, what does an `output` block do, and how does a calling configuration read an output declared in a child module?

level: juniorimportance: must knowfreq 76%

answer

  1. a module's published return value
  2. private internals, public interface
  3. root prints them, child does not
  4. the caller reads it by module address
  5. module.<block_label>.<output_name>

basics

~20 s

An output block publishes a value out of the module that declares it. Root module outputs are printed after apply and readable with terraform output; a child module's outputs are read by its caller as module.<name>.<output_name>.

solid answer

~40 s

An `output` block is a module's published return value. Inside a module, resources reference each other directly — `aws_subnet.private.id` — but nothing outside the module can address those resources, so anything the caller needs must be exported through an `output`. In the **root** module, outputs are the human/CLI surface: Terraform prints them at the end of `apply` and `terraform output` reads them back from state. In a **child** module, outputs are the programmatic interface: the caller references them as `module.<block_label>.<output_name>`, for example `module.network.vpc_id` for a `module "network"` block that declares `output "vpc_id"`. Child outputs are not printed by `apply` — only root outputs are. An output block creates no infrastructure; its arguments are `value`, plus optional `description`, `sensitive`, and `depends_on`.

code

hcl · 20 lines
hcl
# modules/network/outputs.tf
output "vpc_id" {
  description = "ID of the VPC created by this module"
  value       = aws_vpc.main.id
}

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

resource "aws_security_group" "app" {
  name   = "app"
  vpc_id = module.network.vpc_id
}

# root outputs.tf - re-export so apply prints it
output "vpc_id" {
  value = module.network.vpc_id
}

go deeper

for a junior

Know that an output publishes a value out of a module and that the caller reads it as module.<name>.<output_name>. Be able to write a three-line output block with value and description.

for a middle

Explain the encapsulation rule behind it: module internals are not addressable from outside, so outputs are the only interface. Contrast root outputs (printed, read by terraform output) with child outputs (consumed by the caller only).

for a senior

Show interface discipline in review: narrow outputs rather than whole resource objects, descriptions on every one, and awareness that renaming or deleting an output breaks callers even though it destroys nothing.

for a principal

Own outputs as the versioned API of a shared module estate. Argue for a stable, minimal contract, a deprecation path for renamed outputs, and consistent naming across modules so consumers can compose them without reading each module's source.

## What an output block is An `output` block declares a named value that a Terraform module makes visible outside itself. It is the module's *return value*. The block takes one required argument, `value` — any expression — plus optional `description`, `sensitive`, and `depends_on`. ```hcl output "vpc_id" { description = "ID of the VPC created by this module" value = aws_vpc.main.id } ``` Declaring an output changes nothing about the infrastructure. It does add an edge to Terraform's dependency graph — the output cannot be resolved until `aws_vpc.main` is known — but it neither creates, protects, nor refreshes anything. ## Why the boundary needs it Inside a single module, resources address each other directly: `aws_subnet.private.id`, `aws_instance.web.private_ip`. Those addresses are *module-local*. A parent configuration cannot reach into a child and write `module.network.aws_vpc.main.id` — that address does not exist. The module's resources are encapsulated, and the only names that cross the boundary are the outputs it declares. This is exactly the encapsulation you get from a function: the internals are private, the returned values are public. ## Root outputs versus child outputs The same block behaves differently depending on where it sits. **Root module outputs** are the human and CLI surface. Terraform prints them at the end of a successful `apply`, records them in state, and `terraform output` (or `terraform output <name>`) reads them back later without re-running anything. They are how you answer "what is the load balancer's DNS name?" after the fact. **Child module outputs** are the programmatic interface. They are *not* printed after `apply` — a common surprise. The caller consumes them by module address: ```hcl module "network" { source = "./modules/network" } resource "aws_instance" "app" { subnet_id = module.network.private_subnet_id } ``` The label after `module` in the address is the *block* label in the caller (`network`), not the directory name. If you want a child's value on the terminal, the root module must re-export it with its own output block whose `value` is `module.network.private_subnet_id`. ## Values that are not known yet At plan time an output whose value derives from an attribute the provider has not assigned yet renders as `(known after apply)`. That is normal for anything the cloud allocates — IDs, ARNs, generated DNS names. It only becomes a problem when something that must be resolvable during planning, such as a `for_each` key set, depends on it. ## Practical conventions - **Export the smallest useful thing.** An output returning the whole resource object (`value = aws_vpc.main`) couples callers to every attribute and silently widens the interface; exporting `aws_vpc.main.id` keeps the contract narrow. - **Always write `description`.** It is the module's API documentation and is surfaced by tooling that reads the configuration. - **Treat outputs as a public contract.** Renaming or deleting an output is a breaking change for every caller, even though it destroys nothing. - **`depends_on` on an output** is a rare escape hatch: it makes the output wait for a resource that the value expression does not reference, so a caller that consumes the output also waits. Reach for it only when an implicit dependency genuinely does not exist. - **Outputs are recorded in state.** Adding or changing one shows up in the plan as a change to output values even when no resource changes. ## What outputs are not They are not variables — `variable` blocks are inputs, flowing in from the caller; outputs flow out. They are not a way to pass values between separate configurations that have their own state; that requires reading the other configuration's state or a shared data store, which is a different topic. And they are not a place to compute intermediates for internal use — a value only the module itself consumes belongs in the module's own expressions rather than on its public surface.

  • Why does an output sometimes show as "(known after apply)" in the plan?
    Because its value derives from an attribute the provider only assigns during apply — an ID, ARN, or generated DNS name. Terraform prints the placeholder rather than guessing. That is harmless for display, but if something that must resolve at plan time (a `for_each` key set, for example) depends on that unknown value, the plan fails instead.
  • Does removing an output block change any infrastructure?
    No resource is touched, but it is still a breaking change: any caller referencing `module.<name>.<that_output>` fails to resolve, and the root module's recorded output disappears from state, which the plan reports as an output change. Treat outputs as a published contract and version the module accordingly.
  • How do you surface a child module's output on the terminal after apply?
    Re-export it from the root module: declare a root `output` whose `value` is `module.<name>.<output_name>`. Only root outputs are printed after `apply` and returned by `terraform output`, so a child value has to be forwarded explicitly — there is no flag that dumps every module's outputs.

saying these in an interview costs you the question

  • Thinks outputs are needed to reference resources within the same module
  • Says child module outputs are printed at the end of apply
  • Tries to address module internals directly, like module.network.aws_vpc.main.id
  • Believes declaring an output creates or protects infrastructure
  • Confuses outputs with input variables — direction of flow reversed

context