Skip to content

Outputs and Locals

A local is a named expression computed once and reused inside a configuration, while an output takes a value out of a configuration and makes it visible after apply — together with variable, they are the three distinct kinds of named value in Terraform.

Locals: computing a value once and reusing it

Section titled “Locals: computing a value once and reusing it”

A variable is set by whoever calls the configuration, and an output is exposed to whoever consumes it. A local is neither — it is an internal convenience, a name you give to an expression so you do not have to repeat that expression every time you need its result. Nobody outside the configuration can set a local, and nobody outside the configuration can read one directly either.

locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
vm_count = var.environment == "prod" ? 5 : 2
# Azure Storage Account names must be 3-24 characters, lowercase letters and numbers only
storage_account_name = lower(replace("${local.name_prefix}appdata", "-", ""))
}
resource "azurerm_linux_virtual_machine" "web" {
count = local.vm_count
name = "${local.name_prefix}-web-${count.index}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_D2s_v3"
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.web[count.index].id,
]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index}"
})
}
resource "azurerm_storage_account" "app_data" {
name = local.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = local.common_tags
}

local.common_tags is computed once from var.project and var.environment, then referenced by both azurerm_linux_virtual_machine.web and azurerm_storage_account.app_data. Without it, the same three key-value pairs would have to be copied into every resource block that needs them, and a later change to the tagging scheme would mean hunting down every copy. local.vm_count does the same job for a small conditional: the decision “5 in prod, 2 elsewhere” is written once and referenced by name wherever it is needed. local.storage_account_name earns its keep even more directly — Azure Storage Account names must be 3 to 24 characters, lowercase letters and numbers only, no hyphens, so the expression strips the hyphen out of name_prefix once, in one place, instead of every resource that needs a valid storage account name repeating the same lower(replace(...)) call. A local is never assigned by a caller and never appears in terraform plan as something you can override — it is purely a name for a computed value inside this configuration.

An output block takes a value that exists inside a configuration — usually an attribute of a resource that only becomes known after apply — and makes it available outside that configuration. description documents what the value means, and sensitive = true masks it from ordinary CLI output the same way it does on a variable.

output "web_vm_ids" {
description = "IDs of the virtual machines created for the web tier"
value = azurerm_linux_virtual_machine.web[*].id
}
output "app_data_storage_account_id" {
description = "Resource ID of the storage account used for application data"
value = azurerm_storage_account.app_data.id
}
output "sql_admin_password" {
description = "Administrator login password generated for the Azure SQL server"
value = azurerm_mssql_server.main.administrator_login_password
sensitive = true
}
Terminal window
# List every output and its value
terraform output
# Print one output's value, unquoted for scripting
terraform output web_vm_ids
# Machine-readable output, for piping into jq or another tool
terraform output -json app_data_storage_account_id

terraform output on its own prints every declared output after the most recent apply. terraform output <name> prints just one, and terraform output -json renders the value (or all values, if you omit a name) as JSON, which is what you want when a CI pipeline or another script needs to consume the result programmatically rather than read it as a human. The sensitive = true argument on sql_admin_password behaves exactly like it does on a variable: Terraform prints (sensitive value) in plan/apply output and in a bare terraform output, but terraform output sql_admin_password still prints the real value when you ask for it by name, and the value is still stored in plain text in state. It is display protection for terminals and logs, not access control.

Inside a single root configuration, an output is mostly informational — a convenient way to surface a value for a human running terraform apply to glance at. Its real importance shows up once a configuration stops being a single, self-contained thing. When you split infrastructure into modules, a module has no other way to hand a computed value back to whoever called it except through its output blocks — the Modules module later in this course covers module.<name>.<output> references in depth. When you split infrastructure into independently-deployed Terragrunt units instead, a dependency block reads another unit’s outputs to wire units together without ever hard-coding a value between them — that pattern gets its own treatment in a later Terragrunt module. In both cases, the mechanism is the same one shown here: a value becomes known inside one configuration, an output block exposes it, and something else — a parent module or a dependent unit — consumes it.

flowchart LR
  ve["var.environment"] --> lp["local.name_prefix"]
  vp["var.project"] --> lp
  lp --> ri["azurerm_linux_virtual_machine.web"]
  lp --> rb["azurerm_storage_account.app_data"]
  rb --> ob["output app_data_storage_account_id"]
  ob --> dep["dependency block or module output reference"]
A local feeds two resources, and a resource attribute becomes an output
What is the main difference in purpose between a local and an output?
How does a local differ from a variable?
Why do outputs matter for composing infrastructure, even though modules and Terragrunt dependencies are covered in more depth later
What is terraform output -json useful for