Skip to main content
RunBook Academy

TerraformXIII · Variables, Outputs, and LocalsProduction Terraform

Outputs: The Configuration Interface

Intermediate⏱ ~12 minbash

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

Not yet marked complete on this device.

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:

  1. Persist a value to the state file so it can be retrieved later.
  2. Print the value at the end of terraform apply for the operator.
  3. Make the value consumable by other Terraform configurations via terraform_remote_state or a module block.
  4. 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

  1. 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.

  2. 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.

  3. Output depends on a resource not yet created. A depends_on cycle or a missing data source leaves the output empty. The consumer reads an empty value and the apply fails downstream.

  4. CI logs -raw of 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.

  5. terraform_remote_state points 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.

  6. count or for_each changes 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 description is 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.tf file. 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 -json as 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

  1. You remove an output that another stack consumes via terraform_remote_state. What does the consumer see at the next plan?
  2. A sensitive output feeds a non-sensitive module input. What does Terraform do, and why?
  3. CI runs terraform output -raw db_endpoint and pipes the result to a log. What is the production risk?
  4. You rename vpc_id to vpc_identifier in the network stack. The app stack still reads vpc_id. What is the right migration sequence?

Knowledge check · 7 questions

  1. Q1. What is the primary purpose of an `output` block?

  2. Q2. Outputs are required to consume resources from another Terraform state.

  3. Q3. How do you suppress a sensitive output in CLI rendering?

  4. Q4. Which command reads outputs in a machine-friendly format for CI?

  5. Q5. Which of the following are valid surfaces to which outputs persist? (Select all that apply.)

  6. Q6. A downstream stack consumes a sensitive output and passes it to a non-sensitive module input. What happens?

  7. 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.