Outputs and Locals
The idea in one sentence
Section titled “The idea in one sentence”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.
Outputs: exposing a value after apply
Section titled “Outputs: exposing a value after apply”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}# List every output and its valueterraform output
# Print one output's value, unquoted for scriptingterraform output web_vm_ids
# Machine-readable output, for piping into jq or another toolterraform output -json app_data_storage_account_idterraform 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.
Outputs as the mechanism for composition
Section titled “Outputs as the mechanism for composition”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"]