TerraformXIII · Variables, Outputs, and LocalsProduction Terraform
Outputs: The Configuration Interface
What you'll learn
- Define outputs as the contract between one stack and its consumers
- Mark outputs sensitive when they expose secrets or internal attributes
- Hand off values between stacks via `terraform_remote_state` and modules
- Read outputs from CI in a machine-friendly form
- Recognise the failure modes of unstable or undocumented outputs
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
An output is the contract between one Terraform stack and the rest of the world. A VPC ID, a database endpoint, an ALB DNS name — these are the values other stacks, other teams, other pipelines consume. An output that is wrong, missing, accidentally sensitive, or silently unstable is a production incident waiting to surface.
A real production incident: the team renamed an output from db_host to
db_endpoint during a refactor. The app stack consumed db_host via
terraform_remote_state. The app plan failed with Unsupported attribute: db_host and the engineer, on a deadline, hand-edited the
state file. The hand edit corrupted three downstream stacks. The fix
was a coordinated rename across the network and app repos, with the
output alias preserved for one release.
What outputs do
An output block has four responsibilities:
- Persist a value to the state file so it can be retrieved later.
- Print the value at the end of
terraform applyfor the operator. - Make the value consumable by other Terraform configurations via
terraform_remote_stateor amoduleblock. - Make the value consumable by CI/CD pipelines via
terraform output -json.
# CONFIGURATION
output "vpc_id" {
value = aws_vpc.main.id
description = "ID of the production VPC. Consumed by the application stack."
}
output "db_endpoint" {
value = aws_db_instance.prod.endpoint
description = "Endpoint of the production RDS instance. Sensitive."
sensitive = true
}
output "instance_private_ips" {
value = aws_instance.app[*].private_ip
description = "Private IPs of the application instances, in launch order."
}
The description field is mandatory in the production pattern. It is
the documentation of the contract. Consumers read the description to
know what they are getting.
Handoff between stacks
The cross-stack pattern uses terraform_remote_state. The consumer
reads the producer’s state file from a shared backend.
# CONFIGURATION — consumer stack
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "acme-tf-state"
key = "network/production/terraform.tfstate"
region = "eu-west-2"
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.app.id
instance_type = "t3.medium"
subnet_id = data.terraform_remote_state.network.outputs.vpc_id
tags = {
Name = "app-${var.environment}"
}
}
The discipline: the consumer references the output by name. Renaming or removing the output breaks the consumer at the next plan. Renames must be coordinated; removals must wait until every consumer has migrated.
# READ-ONLY — find every consumer of an output
grep -r "remote_state.network.outputs" ../app/
grep -r "module.network.outputs" ../app/
Handoff between modules
Within a single configuration, modules communicate through inputs and outputs. The module that owns the resource declares the output; the parent configuration passes it to the consumer module as an input.
# CONFIGURATION — modules/networking/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
description = "ID of the VPC created by this module."
}
# modules/app/main.tf
module "network" {
source = "../modules/networking"
cidr_block = "10.0.0.0/16"
}
resource "aws_instance" "app" {
subnet_id = module.network.vpc_id
}
Consumption from CI
CI pipelines read outputs in JSON form. The -json flag produces
machine-friendly output; the -raw flag produces the literal value
(use with care on sensitive outputs).
# READ-ONLY
terraform output -json
# {
# "vpc_id": { "value": "vpc-0123456789abcdef0", "type": "string" },
# "db_endpoint": { "value": "(sensitive value)", "type": "string", "sensitive": true },
# "instance_private_ips": { "value": ["10.0.1.10", "10.0.1.11"], "type": ["list", "string"] }
# }
# READ-ONLY — extract one value
terraform output -json | jq -r '.vpc_id.value'
# vpc-0123456789abcdef0
# DESTRUCTIVE for sensitive outputs — renders the value
terraform output -raw db_endpoint
# prod-db.cluster-abc123.eu-west-2.rds.amazonaws.com:5432
The -raw flag bypasses the sensitive redactor. A CI script that pipes
-raw to a log file leaks the value. The pattern is: never pipe -raw
output of a sensitive value to a logger.
WhyThisMatters
WhyThisMatters Outputs are the API of your Terraform configuration. The team that treats outputs as an internal implementation detail ends up with downstream stacks that break in production. The team that treats outputs as a public API — versioned, documented, deprecated — ends up with downstream stacks that survive refactors.
Failure modes
-
Output renamed or deleted. The consumer stack fails at the next plan with
Unsupported attribute. The fix is a coordinated rename with a deprecation window. -
Sensitive output feeds non-sensitive input. The consumer marks the input as not sensitive. The plan errors with “Sensitive value not allowed in non-sensitive argument.” The fix is to mark the consumer input sensitive too.
-
Output depends on a resource not yet created. A
depends_oncycle or a missing data source leaves the output empty. The consumer reads an empty value and the apply fails downstream. -
CI logs
-rawof a sensitive output. The pipeline writes the database endpoint to a log file. The endpoint is not as secret as a password but the discipline should be uniform. -
terraform_remote_statepoints at a stale key. The producer moved to a new state layout; the consumer still reads the old key. The plan fails or reads garbage. The fix is to update the consumer in the same PR as the producer’s move. -
countorfor_eachchanges the output type. A resource that was a single value becomes a list (or vice versa). The consumer’s type expectation breaks. The fix is to keep the count stable or to version the output name (instance_ips_v2).
Production guidance
- Document every output. The
descriptionis the contract. A consumer reads it to know what the value means. - Mark outputs sensitive when warranted. Database endpoints, API keys, ARNs of internal services, IPs of internal-only resources.
- Group outputs into a stable
outputs.tffile. The file is the interface; treat it like an API. - Coordinate renames. A deprecation period with both old and new output names, removed after every consumer migrates.
- Smoke-test in CI. Run
terraform output -jsonas a step; if the output is missing or wrong, fail the build before the apply. - Use
terraform_remote_state, not raw state files. Hand-copied state files break the contract.
What comes next
The next lesson is Locals: Internal Variables — values that are computed by the configuration, not set by the operator, and the discipline of choosing between a variable, a local, and an output.
Verification
- You remove an output that another stack consumes via
terraform_remote_state. What does the consumer see at the next plan? - A sensitive output feeds a non-sensitive module input. What does Terraform do, and why?
- CI runs
terraform output -raw db_endpointand pipes the result to a log. What is the production risk? - You rename
vpc_idtovpc_identifierin the network stack. The app stack still readsvpc_id. What is the right migration sequence?
Knowledge check · 7 questions
Q1. What is the primary purpose of an `output` block?
Q2. Outputs are required to consume resources from another Terraform state.
Q3. How do you suppress a sensitive output in CLI rendering?
Q4. Which command reads outputs in a machine-friendly format for CI?
Q5. Which of the following are valid surfaces to which outputs persist? (Select all that apply.)
Q6. A downstream stack consumes a sensitive output and passes it to a non-sensitive module input. What happens?
Q7. You removed an output from the network stack and the app stack fails with `Unsupported attribute`. What is the most likely root cause?
Passing score: 75%. Answers are checked in this browser.