Terraform Local Values with Examples

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:

hcl
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:

hcl
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.

NOTE
Work in ~/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:

hcl
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:

hcl
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:

text
local.name_prefix
local.tags

A 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:

hcl
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:

bash
cd ~/terraform-labs/terraform-locals/main && terraform apply -auto-approve -input=false -no-color
output
# 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:

bash
cd ~/terraform-labs/terraform-locals/main && echo 'local.name_prefix' | terraform console
output
"dev-api"

The tags map is a local object you can reuse in any block that accepts a map:

bash
cd ~/terraform-labs/terraform-locals/main && echo 'local.tags' | terraform console
output
{
  "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:

bash
cd ~/terraform-labs/terraform-locals/main && echo 'local.display_label' | terraform console
output
"dev-api:myapp"

The boolean local evaluates to false when var.environment is dev:

bash
cd ~/terraform-labs/terraform-locals/main && echo 'local.is_production' | terraform console
output
false

Refactor 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:

hcl
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:

hcl
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":

bash
cd ~/terraform-labs/terraform-locals/refactor-before && terraform plan -input=false -no-color
output
# 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:

bash
cd ~/terraform-labs/terraform-locals/refactor-after && terraform plan -input=false -no-color
output
# 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:

bash
cd ~/terraform-labs/terraform-locals/errors/locals-dot-syntax && terraform validate
output
Error: 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:

bash
cd ~/terraform-labs/terraform-locals/errors/duplicate-name && terraform validate
output
Error: 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:

bash
cd ~/terraform-labs/terraform-locals/errors/cycle && terraform validate
output
Error: 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:

bash
cd ~/terraform-labs/terraform-locals/errors/undeclared-var && terraform validate
output
Error: 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


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.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)