Terraform Cheatsheet

Outputs

Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Declaring Outputs

# outputs.tf
output "instance_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "db_endpoint" {
  value     = aws_db_instance.main.endpoint
  sensitive = true
}
ArgumentRequiredPurposeSince
valueyesthe expression to expose0.x
typenotype constraint on the value1.15
descriptionnodocuments the output, surfaced by the registry0.x
sensitivenoredact from CLI output0.14
depends_onnoexplicit ordering, rare0.x
preconditionnoassert before the value is finalised1.2
ephemeralnonever written to state, module-only1.10
deprecatednowarn when a caller references it, child modules only1.15

Outputs serve two distinct jobs. In the root module they are the CLI and scripting surface, and they are recorded in state. In a child module they are the only values a parent can read, since a parent cannot reach inside a module to a resource attribute.

Reading Outputs

terraform output                       # all outputs, HCL-ish format
terraform output instance_public_ip    # one output, quoted
terraform output -raw instance_public_ip   # bare string, no quotes
terraform output -json                 # every output with type and sensitivity
terraform output -json | jq -r '.db_endpoint.value'

-raw only works on a string, number or bool. For anything structured use -json and pipe through jq.

# Feed a value into another tool
DB_HOST=$(terraform output -raw db_endpoint)
kubectl create secret generic db --from-literal=host="$DB_HOST"

# Export every scalar output as an environment variable
eval "$(terraform output -json \
  | jq -r 'to_entries[] | select(.value.value | type == "string")
           | "export TF_OUT_\(.key | ascii_upcase)=\(.value.value | @sh)")"

terraform output reads state, not config, so it returns the last applied values and needs no provider credentials. It errors if you have never applied.

Outputs From count and for_each Resources

resource "aws_instance" "worker" {
  count = 3
  ami   = var.ami_id
}

output "worker_ips" {
  value = aws_instance.worker[*].private_ip     # list, splat
}

output "first_worker_ip" {
  value = aws_instance.worker[0].private_ip     # errors if count = 0
}

output "maybe_worker_ip" {
  value = one(aws_instance.worker[*].private_ip)  # null when count = 0
}
resource "aws_instance" "app" {
  for_each = toset(["web", "api", "worker"])
  ami      = var.ami_id
}

output "instance_ids" {
  value = { for k, v in aws_instance.app : k => v.id }
}
# => { api = "i-...", web = "i-...", worker = "i-..." }

Prefer a map output over a list whenever the underlying resource uses for_each. A list output reorders when a key is added, which churns every downstream consumer for no reason.

Sensitive Outputs

output "db_password" {
  value     = random_password.db.result
  sensitive = true
}
  • Printed as (sensitive value) by terraform output and in apply summaries.
  • Still readable with terraform output -json or -raw, deliberately, because scripts need it.
  • Still stored in state in plaintext. sensitive is a display control, not encryption.
  • Sensitivity propagates. An output whose value derives from a sensitive input is sensitive whether you said so or not, and Terraform errors if you expose one without the marker. Use nonsensitive(x) only when you have checked that the specific value is safe.
# Ephemeral output (1.10+): a child module may return a secret that
# never reaches state. Root modules cannot declare one.
output "session_token" {
  value     = ephemeral.vault_kv_secret_v2.db.data["token"]
  ephemeral = true
  sensitive = true
}

Preconditions on Outputs

output "api_url" {
  value = "https://${aws_lb.api.dns_name}"

  precondition {
    condition     = aws_lb.api.load_balancer_type == "application"
    error_message = "api_url assumes an ALB, but this is not one."
  }
}

The precondition is checked while the output is finalised, so it fails the apply rather than handing a caller a wrong value. Unlike a resource postcondition, there is no self here, so reference the resource by address.

Consuming Outputs Across Modules

# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}
# root main.tf
module "vpc" {
  source = "./modules/vpc"
}

resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]   # module.<name>.<output>
}

output "vpc_id" {
  value = module.vpc.vpc_id                      # re-export upward
}

With for_each on the module call, outputs are keyed the same way:

module "bucket" {
  for_each = toset(["logs", "backups"])
  source   = "./modules/s3-bucket"
  name     = each.key
}

output "bucket_arns" {
  value = { for k, m in module.bucket : k => m.bucket_arn }
}

Only declared outputs cross the boundary. If a parent needs an attribute, the module must export it, which is why a well-designed module exports IDs, ARNs and names rather than whole resource objects.

Reading Another Root Module's Outputs

Two ways to consume outputs from a separate state file.

# 1. terraform_remote_state: reads the state file directly.
#    Needs read access to the state backend, and sees every output.
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-tf-state"
    key    = "shared/network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}
# 2. tfe_outputs (HCP Terraform): reads only the published outputs,
#    with normal workspace permissions and no state access.
data "tfe_outputs" "network" {
  organization = "my-org"
  workspace    = "network-prod"
}
terraform_remote_statepublished values (tfe_outputs, SSM, etc.)
Grantsfull state read, including other teams' secretsjust the outputs
Couplingtight, breaks on a backend moveloose
Works anywhereyesneeds HCP Terraform or a parameter store

The looser pattern is to publish outputs deliberately, for example writing them to SSM Parameter Store, and read them back with a normal data source. That keeps one team's state out of another team's blast radius.