Outputs
Outputs are a configuration's return values: the attributes you deliberately expose so a human, another module, or a CI step can read them after apply. They are the counterpart to input variables — inputs flow in, outputs flow out — and on a module they are the entire public interface.
When the Hatch ingest pipeline becomes a module, its outputs are what the outside world sees: the raw bucket name, the Pub/Sub topic id, the Cloud Run service URL. Everything else — the IAM bindings, the intermediate names, the API-enablement resources — is internal detail the caller never touches. Deciding what to output is deciding what the module promises.
Declaring an Output
An output block surfaces one value. output "raw_bucket_name" { value = google_storage_bucket.raw.name } exposes the bucket's name; after apply it prints to the CLI and is queryable with terraform output. From a module, that same output is read by the caller as module.ingest.raw_bucket_name — the name you give the output is the name the caller uses.
output "raw_bucket_name" { value = google_storage_bucket.raw.name description = "Name of the raw-events ingest bucket" } output "topic_id" { value = google_pubsub_topic.events.id }
After terraform apply, both print at the end of the run, and terraform output -json renders them as machine-readable JSON for a downstream CI step to consume.
Outputs as the Module Interface
A module's outputs are its contract with the outside world. The caller wires module.ingest.topic_id into the next module's input — the enrichment module's source_topic, say — so outputs are how composed infrastructure passes handles between modules without one reaching into another's internals. The producing module exposes a stable name; the consuming module depends on that name, not on the resource behind it.
The sensitive Flag
sensitive = true redacts a value in plan and apply output, so a generated service-account key or the enrichment-api-key secret payload does not land in CI logs or a pull-request plan comment where it is captured forever. The redaction is real and worth using — but it is log hygiene, not encryption. The value is still stored in plaintext in the state file, which is why the state itself must be protected as if every value were in the clear.
output "sa_key" { value = google_service_account_key.processor.private_key sensitive = true # hides it from CLI output — still plaintext in state }
With sensitive = true, the plan shows (sensitive value) instead of the key. Without it, the private key prints into every log that captures the apply, including CI.
depends_on on an Output
An output can carry depends_on = [google_project_service.run] to force a consumer to wait until a side effect — an API enabled, an IAM binding propagated — has completed, even when the output's own value does not reference that resource. This is how a module signals "I am fully ready," not merely "here is a string." It matters when a downstream consumer would otherwise race ahead and call an API that is not yet enabled.
Use it sparingly. If the output value already references the resource it needs to wait on, Terraform infers the dependency and an explicit depends_on is just noise. It earns its place only for side effects the value itself does not mention.
Output Composition
An output does not have to be a raw passthrough. It can be computed — a constructed service URL, a jsonencode'd summary object, a merged labels map — so a module hands back exactly the shape its callers need instead of leaking every internal attribute. Composing the output is how you keep the module's surface small and intentional even when the resource behind it has dozens of attributes.
Root vs Module Outputs
The same output block plays two roles depending on where it sits. At the root, outputs are for humans and for terraform output -json consumers — they print what an operator wants to see after an apply. Inside a module, they are the only way data escapes the module boundary: a value not declared as an output simply does not exist outside the module, no matter that the resource is right there in the module's code.
Output — a value deliberately exposed as the module's public interface: the bucket name, topic id, service URL a caller needs. Treat the set of outputs as a contract you are committing to keep stable.
Internal value — everything else: intermediate names, IAM bindings, the API-enablement resources. Not output means not visible outside the module, which is the point — the caller depends on the interface, not the internals.
- Outputting a secret — a generated SA key, a Cloud Run service-account token — without
sensitive = true, printing it into CI logs and pull-request plan comments where it is captured forever. - Assuming
sensitive = trueencrypts the value — it only redacts console output; the value sits in plaintext in the state file, which is why state itself must be protected. - Forgetting an output and then trying to reference
module.ingest.bucket_namefrom the parent — if the module never declared it, the value does not exist outside the module no matter that the resource is in its code. - Adding
depends_onto an output for a dependency Terraform could already infer from the value reference — it is only needed for side effects the value doesn't mention, and otherwise just adds noise. - Dumping every raw resource attribute as a separate output, bloating the module's interface and committing you to keeping all of them stable.
- Treat a module's outputs as a deliberate public API: expose the bucket name, topic id, and service URL the caller needs, and nothing internal.
- Mark every credential, key, or secret-derived output
sensitive = true, and still protect state as if the value were in cleartext. - Use
depends_onon an output only to signal completion of a side effect — API enablement, IAM propagation — that the output value doesn't already reference. - Compute outputs into the shape callers actually consume — a full resource URL, a merged labels map — instead of dumping every raw attribute.
- Give every output a
description, since it documents the module's interface for the next engineer who composes it.
Knowledge Check
Why are outputs the only way to read a module's internal value from its parent?
- A value not declared as an output does not exist outside the module boundary, regardless of the resource being in the module's code
- Modules export every internal resource attribute to the parent by default, and outputs merely rename a few of those for readability
- The parent can address any resource inside the child module directly, for example with
module.ingest.google_storage_bucket.raw - Outputs exist only to print to the CLI during apply and cannot be referenced by a parent module
What does sensitive = true on an output actually protect?
- It redacts the value from plan and apply console output; the value is still stored in plaintext in state
- It encrypts the value both in the console output and at rest inside the state file stored on the backend
- It strips the value out of state entirely so the secret is never persisted to disk
- It restricts who is allowed to run
terraform outputby checking their IAM role
When is depends_on on an output justified?
- When a consumer must wait for a side effect — like an API being enabled — that the output's value does not itself reference
- On every single output, as a blanket rule to guarantee correct ordering between modules
- Whenever the output value already references another resource, in order to make that implied dependency explicit and visible
- Only on outputs that are also marked
sensitive
Why compose an output — a constructed URL or a merged map — instead of passing a raw attribute through?
- It hands callers exactly the shape they consume and keeps the module's interface small instead of leaking every internal attribute
- Computed outputs are evaluated and applied faster than raw passthrough outputs
- A raw attribute exposed as an output simply cannot be referenced by a parent module at all, so only a composed value would ever work
- Composition is required before any output can be marked
sensitive
You got correct