Skip to content

Outputs and Locals

An output exposes a value out of a Terraform configuration after apply, while a local computes a named expression once for reuse inside the same configuration — neither one is a variable, and each solves a different problem.

An output block takes a name, a value expression, and optionally a description and sensitive flag. After terraform apply finishes, every declared output is printed, and you can query it again later without re-running apply.

resource "google_compute_instance" "web" {
name = "web-server"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}
output "instance_id" {
description = "The unique identifier Google Cloud assigned to the instance"
value = google_compute_instance.web.instance_id
}
output "instance_external_ip" {
description = "Public IP address of the web server"
value = google_compute_instance.web.network_interface[0].access_config[0].nat_ip
}
output "db_password" {
description = "Generated password for the application database user"
value = random_password.db.result
sensitive = true
}
Terminal window
# Print every output declared in the configuration
terraform output
# Print a single output by name, unquoted for use in scripts
terraform output instance_external_ip
# Print all outputs as JSON, for piping into another tool
terraform output -json

terraform output -json matters more than it looks: it turns your outputs into structured data that another script, a CI pipeline, or a wrapper tool can parse reliably, instead of scraping human-formatted terminal text. The sensitive = true flag on db_password works exactly like it does on a variable — Terraform masks the value in terraform output and in apply output, but the value is still written to the state file in plain text. It is a display protection, not an access control.

Terraform has three distinct kinds of named values, and it is worth being precise about the difference. A variable is set by the caller from outside. An output is exposed to the outside after apply. A local is neither: it is a named expression computed once, inside the configuration, purely so you do not have to repeat that same expression in five different resource blocks.

variable "environment" {
type = string
description = "Deployment environment name"
default = "dev"
}
variable "project_id" {
type = string
description = "GCP project ID"
}
locals {
name_prefix = "${var.project_id}-${var.environment}"
common_labels = {
environment = var.environment
managed_by = "terraform"
}
}
resource "google_compute_instance" "web" {
name = "${local.name_prefix}-web"
machine_type = "e2-medium"
zone = "us-central1-a"
labels = local.common_labels
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}
resource "google_storage_bucket" "assets" {
name = "${local.name_prefix}-assets"
location = "US"
labels = local.common_labels
}

Without local.name_prefix and local.common_labels, that same "${var.project_id}-${var.environment}" string interpolation and that same labels map would have to be typed out again on every resource that needs them, and a later change to the naming scheme would mean hunting down every copy. A locals block is not user-settable the way a variable is, and it is not visible outside the configuration the way an output is — it exists purely to keep a computed expression in exactly one place.

Inside a single root configuration, an output is mostly informational — a convenient way to print a value at the end of apply, or to feed a script via terraform output -json. Its real importance shows up once you start splitting infrastructure into smaller, separately-managed pieces.

When one Terraform module calls another, the calling module reads the child module’s outputs to wire values between them — a networking module’s output "subnet_self_link" becomes the input to a compute module’s variable "subnet". The Modules module later in this course covers that composition in depth. The same idea shows up again, at a larger scale, in Terragrunt: an independently-applied Terragrunt unit reads another unit’s outputs through a dependency block, so a VPC deployed in one directory can hand its network name to a database deployed in a completely separate directory, with no shared Terraform state. Outputs are what make both of these forms of composition possible — without a defined, stable output, there is nothing for the consuming module or unit to read.

flowchart LR
  v1["var.project_id"] --> l["local.name_prefix"]
  v2["var.environment"] --> l
  l --> r1["google_compute_instance.web"]
  l --> r2["google_storage_bucket.assets"]
  r1 --> o["output ip_address"]
  o --> ext["Consumed by another module or a Terragrunt dependency block"]
A local computes a shared value once; an output exposes the final result after apply
What is the main purpose of a locals block, compared to variable and output?
Why do outputs matter even in a single root configuration with no modules yet
What is terraform output -json most useful for
How does an output later feed a Terragrunt dependency block, at a high level