Skip to main content
Terraform · lesson 6 of 17
Terraform beginner Lesson 6 of 17

Terraform Locals: Computed Values Without Repetition

A locals block computes a value once from variables or other locals, so one edit cascades everywhere, shown with the real replacement plan it causes.

By SkillByExample · Updated

terraform locals hcl

Any value used more than once can end up with one copy updated and another forgotten. A locals block computes the value once, and everything else refers to it by name. Examples use Terraform 1.16.5 and hashicorp/local 2.9.1.

A locals block with a dependency chain

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

locals {
  environment = "staging"
  app_name    = "orders-api"

  # A local can reference another local
  full_name = "${local.app_name}-${local.environment}"

  # And do real computation, not just string joins
  replica_count = local.environment == "production" ? 5 : 2
}

resource "local_file" "config" {
  filename = "${path.module}/generated-config.txt"
  content  = "service=${local.full_name}\nreplicas=${local.replica_count}\n"
}

output "resolved_name" {
  value = local.full_name
}

output "resolved_replicas" {
  value = local.replica_count
}
$ terraform apply -auto-approve -no-color

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.config will be created
  + resource "local_file" "config" {
      + content              = <<-EOT
            service=orders-api-staging
            replicas=2
        EOT
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./generated-config.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + resolved_name     = "orders-api-staging"
  + resolved_replicas = 2
local_file.config: Creating...
local_file.config: Creation complete after 0s [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

resolved_name = "orders-api-staging"
resolved_replicas = 2

The strings orders-api-staging and 2 aren’t written anywhere in the config. They’re computed, and the plan shows the computed values, not local.full_name. Locals are known at plan time when everything they depend on is known, which is why the outputs already have values in the plan.

Changing one local cascades everywhere it’s used

Change one line, environment = "production", and apply again:

$ terraform apply -auto-approve -no-color
local_file.config: Refreshing state... [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.config must be replaced
-/+ resource "local_file" "config" {
      ~ content              = <<-EOT # forces replacement
          - service=orders-api-staging
          + service=orders-api-production
          - replicas=2
          + replicas=5
        EOT
      ~ content_base64sha256 = "UAN/k0R8sD0QmwoUrlr9DP9ump56zzTSIOyJsQf7lSs=" -> (known after apply)
      ~ content_base64sha512 = "GjrYwJJJsaDlqMvqCMEjEBzjZTWRc66BE8u/YvKuek7jUOZPJg80qzAS8p7iCkHZ3V1Xhy+R4eGYsm4SA+XqUQ==" -> (known after apply)
      ~ content_md5          = "8abf6f91dba1c78ccdfe2b1e6a362b31" -> (known after apply)
      ~ content_sha1         = "d28c70f13bcd999b995544b3c05e8114f0fbc772" -> (known after apply)
      ~ content_sha256       = "50037f93447cb03d109b0a14ae5afd0cff6e9a9e7acf34d220ec89b107fb952b" -> (known after apply)
      ~ content_sha512       = "1a3ad8c09249b1a0e5a8cbea08c123101ce365359173ae8113cbbf62f2ae7a4ee350e64f260f34ab3012f29ee20a41d9dd5d57872f91e1e198b26e1203e5ea51" -> (known after apply)
      ~ id                   = "d28c70f13bcd999b995544b3c05e8114f0fbc772" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Changes to Outputs:
  ~ resolved_name     = "orders-api-staging" -> "orders-api-production"
  ~ resolved_replicas = 2 -> 5
local_file.config: Destroying... [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]
local_file.config: Destruction complete after 0s
local_file.config: Creating...
local_file.config: Creation complete after 0s [id=5cf2018b13895ce8b2713b79d815ec152fded303]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:

resolved_name = "orders-api-production"
resolved_replicas = 5

Both full_name and replica_count were recomputed, and the conditional flipped from 2 to 5. The -/+ comes from local_file itself, which can’t edit a file’s content in place. A different resource type might apply the same change as an in-place update.

A local can’t be set from outside

$ terraform plan -var full_name=custom -no-color

Error: Value for undeclared variable

A variable named "full_name" was assigned on the command line, but the root
module does not declare a variable of that name. To use this value, add a
"variable" block to the configuration.

That’s the line between a variable and a local. A variable is an input the caller controls. A local is internal, and nothing on the command line can override it. If callers need to override a value, make it a variable and compute the local from it.

Practice

1. Add a boolean local is_production and expose it as an output called is_prod.

Solution
locals {
  is_production = local.environment == "production"
}

output "is_prod" {
  value = local.is_production
}
$ terraform apply -auto-approve -no-color
local_file.config: Refreshing state... [id=5cf2018b13895ce8b2713b79d815ec152fded303]

Changes to Outputs:
  + is_prod           = true

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

is_prod = true
resolved_name = "orders-api-production"
resolved_replicas = 5

A configuration can have several locals blocks, and a local can hold any type, not just strings. Adding an output changes nothing real, so the apply only records the new output.

2. Rewrite the original locals block in reverse order, with replica_count first and environment last. Does it still work?

Solution
locals {
  replica_count = local.environment == "production" ? 5 : 2
  full_name     = "${local.app_name}-${local.environment}"
  app_name      = "orders-api"
  environment   = "production"
}
$ terraform validate -no-color
Success! The configuration is valid.

$ echo 'local.replica_count' | terraform console
5

Order doesn’t matter. Terraform builds a graph from the references, so replica_count waits for environment no matter where each one is written.

3. Add an output that references local.does_not_exist and validate.

Solution
output "broken" {
  value = local.does_not_exist
}
$ terraform validate -no-color

Error: Reference to undeclared local value

  on main.tf line 35, in output "broken":
  35:   value = local.does_not_exist

A local value with the name "does_not_exist" has not been declared.

A misspelled local fails validation. It never quietly becomes an empty string in a resource name.

4. Build a tags map local that always has app and env, and adds pager = "on-call" only in production. Inspect it with terraform console.

Solution
locals {
  tags = merge(
    { app = local.app_name, env = local.environment },
    local.environment == "production" ? { pager = "on-call" } : {},
  )
}
$ echo 'local.tags' | terraform console
{
  "app" = "orders-api"
  "env" = "production"
  "pager" = "on-call"
}

merge with a conditional empty map is the usual way to add keys only in some environments. Every resource that sets tags = local.tags then gets the same set.

Next: modules, which package a group of resources, their variables and their outputs into a reusable unit.

Frequently Asked Questions

What's the difference between a variable and a local?
A variable is an input that the caller sets from outside, through a .tfvars file, a -var flag or a TF_VAR_ environment variable. A local is derived inside the configuration from variables, resource attributes or other locals, and the caller can never set it directly; passing -var for a local's name fails with 'Value for undeclared variable'.
Can a local reference another local?
Yes, as long as the references don't form a cycle. Terraform builds a dependency graph from the references themselves, so the order of entries inside a locals block, or across several locals blocks, doesn't matter.
Why did changing one local replace a resource instead of updating it?
That comes from the resource type receiving the value, not from locals. local_file treats any change to content as requiring a new file, so the plan shows -/+. A different resource type might update the same kind of change in place. Locals only decide what value flows in.
Do locals show up in terraform state?
No. Locals are recomputed from scratch on every plan and apply, and nothing about a locals block is persisted. Only the resources built from a local's value, and any output that exposes it, end up in state.
When should I not use a local?
When the value is used exactly once and the expression is short, inlining it is clearer than making readers jump to a locals block. Locals pay off when a value is used in several places, or when naming a complex expression makes the resource that uses it easier to read.