Skip to content
beginner Phase 3 · Resources, Data Sources, Outputs, and Dependencies

Output Value Declarations

Exposing computed values with descriptions, sensitivity, and conditional display.

40m
0 problems
Topic Progress 0%

Outputs: The Contract Between Your Stack and Everyone Else

A stack that creates infrastructure but publishes nothing is a black box: the load balancer's DNS name exists somewhere in a state file, and every consumer — a deployment pipeline, a teammate, another stack — comes asking for it. Output blocks are the deliberate interface that ends the asking:

output "alb_dns_name" {
  description = "DNS name of the public load balancer for service traffic"
  value       = aws_lb.public.dns_name
}

output "private_subnet_ids" {
  description = "Subnet IDs the application tier attaches to"
  value       = aws_subnet.private[*].id
}

Every apply prints them, which is the point: after a successful run the addresses and IDs you will actually need are on screen, not buried in state JSON:

Apply complete! Resources: 3 added, 1 changed, 0 destroyed.

Outputs:

alb_dns_name = "acme-public-1234567890.eu-west-1.elb.amazonaws.com"
private_subnet_ids = ["subnet-0aaa", "subnet-0bbb"]

Between applies, the CLI reads them from state: terraform output lists everything, terraform output -raw alb_dns_name returns a single unquoted value safe to embed in a shell command, and terraform output -json returns the full set with types and sensitivity flags for tooling. The -raw distinction matters more than it looks: without it Terraform quotes and escapes strings, so a naive curl $(terraform output alb_dns_name) sends literal quote characters to your endpoint — a bug that appears exactly once, in a production pipeline, at the worst time.

The description argument is the documentation channel. It is what teammates read in terraform output -json, what module users see when deciding which value to wire where, and what makes an interface discoverable without opening the source. Outputs also compose: values may be any expression, so an output can aggregate resources (aws_subnet.private[*].id), compute from other outputs' inputs, or wrap values with format() into ready-to-use strings — the output block is where raw attributes become usable values.

One structural rule to internalize now: outputs declared in child modules are not printed and not visible to terraform output; they exist only as inputs to whatever module consumed them, reachable in the root as module.<name>.<output_name>. Values become operator-facing only when the root module re-declares them — which is why mature configurations carry a tidy wall of root outputs mirroring the values their consumers need, each with a description, forming a documented contract between the infrastructure and the humans and pipelines on the other side of it.

A final scope note: terraform output reads the current directory's state, so running it in a workspace or directory you have never applied fails with a missing-state error rather than returning empties — outputs are a property of an applied stack, not of configuration files. That is also why output queries work offline (no provider calls, no API credentials needed for the read) and why they are safe to run in lightweight pipeline steps that only have state access. When you need values before an apply — to seed a variable or make a decision — outputs are the wrong tool; that is a variables or data-source question. The next chapter follows the values to their consumers — pipelines, other stacks, and the assertions that keep a published contract honest.

Sensitive: What the Flag Masks, and What It Never Protects

Some values must leave a stack — a generated database password, a KMS key ID, an API token — and Terraform gives you one mechanism for handling them:

output "db_password" {
  description     = "Generated password for the application database"
  value           = aws_db_instance.app.password
  sensitive       = true
}

The mechanics are worth stating precisely, because the flag's boundaries are a favorite interview trap. Marking an output sensitive = true does three things: the value is redacted in CLI listings (printed as (sensitive value)), it is redacted in terraform output -json, and it propagates: any expression that consumes a sensitive value becomes sensitive itself, so downstream outputs and resources referencing it must also be declared sensitive or the apply fails with "Output refers to sensitive values". That propagation is Terraform refusing to let a secret leak through an output nobody thought about — the error is the feature.

What the flag does not do is protect state. The plaintext value of every sensitive output lives in the state file, and so does the underlying resource attribute; sensitivity is a display classification, not encryption. With a remote backend, other stacks that read your state via the terraform_remote_state data source receive the value too — marked sensitive, so their own handling obligations propagate, but received nonetheless. The practical consequences follow directly: state access control (backend permissions, encryption at rest, audit logging) is your actual secret boundary, and the output flag is the courtesy layer that keeps secrets off screens and out of logs. Teams that grasp this stop putting long-lived credentials in outputs entirely, preferring to read a secret from a secrets manager by reference at deploy time — the output then carries the reference, not the secret.

Handling in pipelines is its own discipline: terraform output -raw db_password still prints the value to whoever runs it — deliberately, since that is how a migration job reads it — so wrap the command where its output feeds the next process rather than into a log. A pattern that satisfies auditors while keeping the workflow: the stack publishes the secret's name (output "db_secret_id" { value = aws_secretsmanager_secret.db.name }) and the deployment job reads the value itself with scoped credentials — now the secret never appears in Terraform output at all, and every read is attributable in the secrets manager's audit log. Review your sensitive surface regularly with terraform output -json | jq 'to_entries[] | select(.value.sensitive != true)': a listing of everything your stack publishes in the clear, which should be short and boring.

Sensitivity has one more propagation path worth naming because it surprises module authors: a sensitive variable makes everything derived from it sensitive too, all the way through resources and out to outputs. The practical consequence is that a single sensitive = true on a password input can cascade into "Output refers to sensitive values" errors three resources downstream when someone references the derived value in a non-sensitive output. The fix is always the same discipline — mark the consumer sensitive, or stop passing the raw secret through and pass a reference instead.

Who Actually Consumes Outputs: Remote State, Pipelines, and Preconditions

An output is only as valuable as its consumers, and there are three, each with a distinct access pattern.

Pipelines and humans read outputs through the CLI — the terraform output [-raw|-json] forms from the first chapter. The idiom that survives contact with production embeds outputs directly into deployment steps:

URL=$(terraform output -raw alb_dns_name)
aws s3 cp ./app.zip "s3://$(terraform output -raw artifact_bucket)/app.zip"

-raw everywhere, one command per value, no parsing. For structured values (lists, maps), -json plus jq is the honest tool — trying to slice JSON with shell string operations is a bug generator.

Other Terraform stacks read outputs through the terraform_remote_state data source, which treats another stack's entire state as readable input:

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-tfstate"
    key    = "network/terraform.tfstate"
    region = "eu-west-1"
  }
}

resource "aws_security_group" "app" {
  vpc_id = data.terraform_remote_state.network.outputs.vpc_id
}

Note the sharp edge this creates, and be able to name it in an interview: remote state is a hard dependency between stacks, both at plan time (the producer's state must be readable and current) and in blast radius (the consumer sees everything in the producer's state — every secret, every resource — not just what was published as outputs). Teams that want a narrower contract publish the values through a module interface, a parameter store, or a dedicated "interface stack" instead of raw state sharing; the dependency direction — network before application — is always encoded by this data source, which is also why stack topologies are usually drawn as graphs during design.

Root configuration logic consumes child module outputs via module.<name>.<output>, the flow you saw in the first chapter — child outputs flow up, root outputs flow out. And modern Terraform lets an output carry an assertion of its own: an output precondition (Terraform 1.2+) fails the apply when a published value violates an expectation —

output "az_count" {
  value = length(var.azs)

  precondition {
    condition     = length(var.azs) >= 2
    error_message = "Stack contract requires at least two availability zones."
  }
}

which converts "we noticed the stack published something wrong" from an incident into a failed apply with your message attached. Preconditions on outputs (and on resources and data sources more generally) are Terraform's answer to "where should this assertion live": on the object that owns the fact, as close as possible to where it can be corrected. The example above catches a misconfigured availability-zone list at the stack that chose it, rather than in the application stack that merely consumed it hours later — a placement difference that shows up directly in mean time to repair.

The generalization closes the topic: outputs are the stack's published API, and APIs deserve descriptions, sensitivity discipline, and — where it matters — assertions. A stack whose outputs are described, minimal, and asserted is a stack other people can build on without reading its source; a stack that publishes thirty undocumented values has merely moved its coupling problem one directory over. Values a stack reads instead of publishes come from the cloud's own inventory, which is the next topic: Data Source Lookups.

Quiz

1. An output references a resource attribute marked sensitive, but the output itself has no sensitive flag. What happens?

Question 1 options

2. What does sensitive = true protect, and where does the plaintext value still exist?

Question 2 options

3. Stack B needs the VPC ID from stack A. Which statement is accurate?

Question 3 options

4. What is the effect of a precondition inside an output block?

Question 4 options

Flashcards

Question

How do you read outputs from the CLI, and why does -raw matter for scripts?

Answer

`terraform output` (all, quoted) · `terraform output -raw <name>` (single unquoted value — safe in shell substitutions) · `terraform output -json` (structured, includes sensitivity flags). Without -raw, strings come back JSON-quoted and escaped, corrupting `curl $(terraform output name)`-style commands.

Question

What three effects does sensitive = true have, and what does it not do?

Answer

It redacts the value in CLI/-json listings, propagates sensitivity to every downstream consumer (undeclared use errors with "Output refers to sensitive values"), and flags the value for tooling. It does NOT remove the plaintext from state or from remote-state readers — protection there is the backend's access control and encryption.

Question

What is the child-module output rule?

Answer

Outputs declared in a child module are not printed after apply and are not visible via `terraform output`; they are internal inputs, referenced from the root as `module.<name>.<output>`. Operator-facing values must be re-declared as root outputs — which is why root modules carry a documented wall of outputs with descriptions.

Question

What dependency does the terraform_remote_state data source create, and what is its hidden cost?

Answer

It makes the consumer stack depend on the producer's state at plan time (backend must be readable and current), and it exposes the producer's *entire* state — all resources and secrets, not just outputs — to the consumer. Sensitive outputs arrive marked sensitive, but they arrive.

Question

What does an output precondition do and when does it run?

Answer

An assertion inside an output block (Terraform 1.2+) checked during apply: if the condition is false, the apply fails with your error_message and the violating value is not published. Use it to enforce the stack's published contract (e.g. "at least two AZs") instead of letting consumers discover violations.

Revision Notes

Key Takeaways

  • 1. Outputs print after every apply and are queryable between them; `-raw` is the script-safe form, `-json` the tooling form, and descriptions are the documentation
  • 2. Sensitivity propagates like a type: consuming a sensitive value forces the consumer to declare itself or fail the apply — a deliberate leak guard
  • 3. `sensitive = true` masks display; it never removes plaintext from state or from remote-state readers — backend access control and encryption are the actual secret boundary
  • 4. Child module outputs are internal to the module tree; only root outputs reach operators and pipelines, which is why root modules carry a deliberate, described output wall
  • 5. `terraform_remote_state` encodes stack order but exposes the producer's full state; output preconditions (1.2+) turn contract violations into failed applies with your message

Interview Tips

  • • "How do consumers get values out of Terraform?" — declared outputs via `terraform output -raw`/`-json` in pipelines, `terraform_remote_state` for other stacks, `module.<name>.<output>` for child modules in the same root; note descriptions as the discoverability layer
  • • "Where do secrets live if outputs are marked sensitive?" — in state, always; the flag only masks display and propagates handling obligations; the actual controls are backend encryption, access control, and preferring secret *references* over secret values in outputs
  • • "What is the risk of terraform_remote_state?" — hard plan-time coupling plus full-state visibility to the consumer; offer the alternatives (publish via module interface, parameter store, interface stack) when blast radius matters
  • • "How do you guarantee a stack never publishes a bad value?" — output preconditions: a condition checked at apply whose failure aborts with your error_message — contract enforcement at the publishing side, where the fix belongs

Cheat Sheet

Outputs

Declaration:

output "alb_dns_name" {
  description = "DNS name of the public load balancer"
  value       = aws_lb.public.dns_name
}

output "db_password" {
  description = "Generated application DB password"
  value       = aws_db_instance.app.password
  sensitive   = true
}

Reading:

Command Result
terraform output all outputs, human-quoted
terraform output -raw <name> one value, unquoted — use in scripts
terraform output -json structured set with sensitivity flags

Rules of thumb: child outputs are internal (module.<name>.<output> in the root); root outputs are the public API — keep them few, described, asserted. sensitive = true masks display and propagates; state remains the secret store, so backend access control is the real boundary. Cross-stack: data.terraform_remote_state.<x>.outputs.<y> — a hard dependency exposing the whole producer state.

Continue Learning

Before You Start

Next Topics

Related Reading