| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Terraform locals blocks, local.NAME references, derived values from variables, maps and objects, functions in locals, conditional expressions, locals referencing locals, multiple locals blocks, refactor equivalence, locals vs variables vs outputs, and common local errors. Does not cover variable assignment, function catalogs, module interface design, or environment architecture. |
| Related guides | Terraform variables Terraform output values Terraform HCL syntax Terraform resources Terraform Associate certification course |
Copying the same expression into three Terraform resource blocks works until the prefix changes. Then you hunt every duplicate:
resource "terraform_data" "a" {
input = "${var.environment}-app"
}
resource "terraform_data" "b" {
input = "${var.environment}-app"
}A local value computes the expression once and reuses it:
locals {
name_prefix = "${var.environment}-app"
}
resource "terraform_data" "a" {
input = local.name_prefix
}
resource "terraform_data" "b" {
input = local.name_prefix
}Reference locals with local.NAME, not locals.NAME. The rest of this guide walks through syntax, derived values, refactor proof, and the errors you are likely to hit on Terraform 1.15.8.
~/terraform-labs/terraform-locals/ on the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before plan or apply. Examples use the built-in terraform_data resource so you do not need cloud credentials.
Terraform locals block syntax and local.NAME
Input variables receive values from outside the module: defaults, terraform.tfvars, -var, or TF_VAR_*. Local values are internal. They exist only inside the module where you define them and are not set from the CLI.
Declare one or more names inside a locals block:
locals {
environment = "dev"
prefix = "app-${local.environment}"
}You may split locals across multiple blocks in the same module. Terraform merges them into one namespace, as if you had written a single block:
locals {
name_prefix = "${var.environment}-${var.application}"
}
locals {
tags = {
Environment = var.environment
Application = var.application
ManagedBy = "terraform"
}
}Reference a local with the singular keyword local:
local.name_prefix
local.tagsA frequent mistake is locals.name_prefix. Terraform parses that as a resource type named locals, not your local value.
Derive locals from variables, maps, and expressions
The main lab at ~/terraform-labs/terraform-locals/main/ builds several derived values from input variables:
terraform {
required_version = ">= 1.12.0"
}
variable "environment" {
type = string
default = "dev"
}
variable "application" {
type = string
default = "api"
}
variable "name" {
type = string
default = " MyApp "
}
locals {
name_prefix = "${var.environment}-${var.application}"
}
locals {
tags = {
Environment = var.environment
Application = var.application
ManagedBy = "terraform"
}
}
locals {
normalized_name = lower(trimspace(var.name))
display_label = "${local.name_prefix}:${local.normalized_name}"
is_production = var.environment == "prod"
}
resource "terraform_data" "example" {
input = {
prefix = local.name_prefix
label = local.display_label
tags = local.tags
prod = local.is_production
}
}
output "name_prefix" { value = local.name_prefix }
output "display_label" { value = local.display_label }
output "tags" { value = local.tags }
output "normalized_name" { value = local.normalized_name }After terraform init and apply, the plan shows how locals flow into the resource input map:
cd ~/terraform-labs/terraform-locals/main && terraform apply -auto-approve -input=false -no-color# terraform_data.example will be created
+ resource "terraform_data" "example" {
+ input = {
+ label = "dev-api:myapp"
+ prefix = "dev-api"
+ prod = false
+ tags = {
+ Application = "api"
+ Environment = "dev"
+ ManagedBy = "terraform"
}
}
}
Outputs:
display_label = "dev-api:myapp"
name_prefix = "dev-api"
normalized_name = "myapp"name_prefix combines two variables. normalized_name uses lower and trimspace on var.name without teaching the full Terraform functions catalog here. display_label chains two locals. is_production compares var.environment to a string and evaluates to a boolean. Locals can also use conditional expressions when the true and false branches produce different values; see Terraform expressions.
Inspect individual locals with terraform console:
cd ~/terraform-labs/terraform-locals/main && echo 'local.name_prefix' | terraform console"dev-api"The tags map is a local object you can reuse in any block that accepts a map:
cd ~/terraform-labs/terraform-locals/main && echo 'local.tags' | terraform console{
"Application" = "api"
"Environment" = "dev"
"ManagedBy" = "terraform"
}A local may reference another local in the same module, regardless of which locals block contains it or where it appears in the files, as long as the references do not form a cycle:
cd ~/terraform-labs/terraform-locals/main && echo 'local.display_label' | terraform console"dev-api:myapp"The boolean local evaluates to false when var.environment is dev:
cd ~/terraform-labs/terraform-locals/main && echo 'local.is_production' | terraform consolefalseRefactor repeated expressions without changing the plan
Locals are a maintainability tool. The before and after configurations should produce the same infrastructure.
Before (refactor-before/), the prefix is duplicated:
variable "environment" {
type = string
default = "dev"
}
resource "terraform_data" "a" {
input = "${var.environment}-app"
}
resource "terraform_data" "b" {
input = "${var.environment}-app"
}After (refactor-after/), one local owns the expression:
locals {
name_prefix = "${var.environment}-app"
}
resource "terraform_data" "a" {
input = local.name_prefix
}
resource "terraform_data" "b" {
input = local.name_prefix
}Plan from the before directory and note both resources receive input = "dev-app":
cd ~/terraform-labs/terraform-locals/refactor-before && terraform plan -input=false -no-color# terraform_data.a will be created
+ resource "terraform_data" "a" {
+ input = "dev-app"
}
# terraform_data.b will be created
+ resource "terraform_data" "b" {
+ input = "dev-app"
}
Plan: 2 to add, 0 to change, 0 to destroy.The after directory produces the same plan shape:
cd ~/terraform-labs/terraform-locals/refactor-after && terraform plan -input=false -no-color# terraform_data.a will be created
+ resource "terraform_data" "a" {
+ input = "dev-app"
}
# terraform_data.b will be created
+ resource "terraform_data" "b" {
+ input = "dev-app"
}
Plan: 2 to add, 0 to change, 0 to destroy.Apply either version and both outputs read dev-app. The refactor removed duplication without changing behavior.
Locals vs variables and outputs
| Input variable | Local value | Output value | |
|---|---|---|---|
| Direction | Into the module | Internal only | Out of the module |
| Reference syntax | var.name |
local.name |
module.NAME.OUTPUT from a caller; terraform output NAME for root outputs |
| Set from CLI / tfvars | Yes | No | No |
| Default in block | Optional default |
N/A (always computed) | N/A |
| Best for | Caller-supplied settings | Derived or repeated expressions | Values to expose after apply |
Use a variable when an operator or parent module must choose the value, such as environment or instance_count. Use a local when the value is computed from those inputs, such as a name prefix or tag map. Use an output when you need to publish a result after apply; see Terraform output values.
Avoid pushing derived strings into variables just to avoid locals. If only your module consumes the value, a local keeps the interface smaller.
Common Terraform locals errors
Reproduce each failure under ~/terraform-labs/terraform-locals/errors/.
locals.foo instead of local.foo
Using the plural form makes Terraform look for a resource type named locals. Validate the misnamed reference:
cd ~/terraform-labs/terraform-locals/errors/locals-dot-syntax && terraform validateError: Reference to undeclared resource
on main.tf line 3, in resource "terraform_data" "x":
3: resource "terraform_data" "x" { input = locals.foo }
A managed resource "locals" "foo" has not been declared in the root module.Duplicate local name
Assigning the same key twice in one locals block fails validation:
cd ~/terraform-labs/terraform-locals/errors/duplicate-name && terraform validateError: Attribute redefined
on main.tf line 4, in locals:
4: foo = "b"
The argument "foo" was already set at main.tf:3,3-6. Each argument may be set
only once.The same name in two separate locals blocks triggers Duplicate local value definition instead. Local names must be unique across the whole module, not per block.
Cyclic local references
When local.a depends on local.b and local.b depends on local.a, validation stops with a cycle error:
cd ~/terraform-labs/terraform-locals/errors/cycle && terraform validateError: Cycle: local.b (expand), local.a (expand)local.a cannot depend on local.b while local.b depends on local.a.
Undeclared variable in a local
A local expression can only reference variables that exist in the module:
cd ~/terraform-labs/terraform-locals/errors/undeclared-var && terraform validateError: Reference to undeclared input variable
on main.tf line 2, in locals:
2: locals { prefix = var.missing }
An input variable with the name "missing" has not been declared. This
variable can be declared with a variable "missing" {} block.Quick troubleshooting reference
| Symptom | Likely cause | Fix |
|---|---|---|
Reference to undeclared resource for locals.x |
Used plural locals. prefix |
Change to local.x |
Attribute redefined / Duplicate local value |
Same local name twice | Rename or merge into one assignment |
Cycle: local.a, local.b |
Locals depend on each other in a loop | Break the cycle; compute from variables or resources |
Reference to undeclared input variable |
Local uses var that was never declared |
Add a variable block or fix the name |
| Locals for values callers must override | Derived value exposed as if it were input | Move to variable or output as appropriate |
References
- Local Values — Terraform documentation
- Input Variables — Terraform documentation
- Output Values — Terraform documentation
- Expressions — Terraform configuration language
Summary
You refactored a repeated "${var.environment}-app" expression into local.name_prefix and confirmed the before and after plans match. Along the way you declared locals in multiple blocks, built maps and conditionals, chained locals together, and applied functions like lower and trimspace without promoting every intermediate string to an input variable.
The syntax detail that trips up most readers is the reference form: local.name, never locals.name. Names must be unique across every locals block in the module, and cycles between locals fail validation before plan.
Use variables for values that cross the module boundary, locals for expressions you compute inside the module, and outputs for values you publish after apply. Next, deepen conditional and string expressions in Terraform expressions, or return to Terraform variables when you need to wire inputs from tfvars and the CLI.

