skip to content

Locals

Locals name intermediate values so expressions stay readable, and they are not configurable from outside. Explaining the difference between a local and a variable is a small but frequent question.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

4

In Terraform, what is a locals block, and how does it differ from an input variable and from an output?

level: juniorimportance: must knowfreq 72%

answer

  1. three constructs, three audiences
  2. who can set it from outside
  3. plural block, singular reference
  4. no tfvars entry exists for a local
  5. output is the module's return value

basics

~20 s

A Terraform locals block names a computed value for reuse inside one module. Unlike an input variable it cannot be set from outside — no tfvars, CLI flag or environment override — and unlike an output it exposes nothing to the module's caller.

solid answer

~40 s

A module has three named value constructs and they differ by who sets the value and who can read it. An **input variable** (`variable` block) is the module's parameter: for a root module it comes from `terraform.tfvars`, `*.auto.tfvars`, a `TF_VAR_` environment variable, `-var` or `-var-file`, or the block's `default`; for a child module it comes from the `module` block that calls it. A **local** (`locals` block) is an internal name for an expression — `local.name_prefix` — visible only inside the module that declares it, and settable from nowhere outside. An **output** (`output` block) is the module's return value, read by a parent as `module.x.y` or printed by `terraform output`. So: caller-supplied is a variable, derived-and-private is a local, exported is an output. Note the block is plural, `locals`, but the reference is singular, `local.`.

go deeper

for a junior

Be ready to state the difference in one breath: a variable comes from the caller, a local is computed inside the module, an output is what the module hands back. Remember the plural block and singular reference.

for a middle

Explain the scoping rules — locals do not cross module boundaries, several locals blocks merge into one namespace, and a locals block accepts no type or validation meta-arguments. Show a computed prefix as the motivating example.

for a senior

Demonstrate the interface judgment: variables and outputs are a module's public contract that you must keep stable, while locals are implementation you can refactor freely. Call out interface bloat from defaults nobody overrides.

for a principal

Own the standard across a repository: which values are legitimately configurable per environment versus derived once, and how that choice determines how many knobs every team must reason about when they call a shared module.

## The three ways a value lives in a module A Terraform module is just a directory of `.tf` files evaluated together. Besides resources and data sources it has exactly three named constructs for values, and the clean way to tell them apart is by asking two questions: *who can set it* and *who can read it*. **Input variable — set by the caller, read inside.** Declared with a `variable` block, it is the module's parameter. For a root module the value arrives from outside Terraform: a `terraform.tfvars` file, an `*.auto.tfvars` file, a `TF_VAR_`-prefixed environment variable, `-var` or `-var-file` on the command line, or the `default` in the block. For a child module it arrives as an argument in the `module` block that calls it. Either way, the caller decides. **Local value — set by the module itself, read inside.** Declared in a `locals` block: ```hcl locals { name_prefix = "${var.project}-${var.environment}" is_prod = var.environment == "prod" } ``` and referenced as `local.name_prefix`. The block keyword is plural and the reference prefix is singular — writing `locals.name_prefix` is one of the most common beginner errors. A module may contain several `locals` blocks; they all contribute to one flat namespace, and two entries with the same name are an error. **Output — set by the module, read by the caller.** An `output` block is the module's return value. A parent reads a child's output as `module.<name>.<output>`; for a root module, `terraform output` prints it and other configurations can read it through remote state. ## What a local is not A local is *not* a configuration knob. Moving a hardcoded string into a `locals` block does not make it overridable: there is no `-var` for a local, no tfvars entry, no environment variable. If someone runs `terraform apply -var 'name_prefix=x'` against a module where `name_prefix` is a local, Terraform rejects it because no variable of that name is declared. A local is also *not* a constant. Its expression can reference input variables, other locals, resource attributes, data sources and module outputs, so its value can depend on what the plan discovers. And a `locals` block takes no meta-arguments: no `type` constraint, no `description`, no `validation`. Those belong to `variable`. If a value needs a declared type or a validation rule, that is a signal it should be an input variable, not a local. ## What locals are actually for Four uses cover most real repositories: 1. **A computed intermediate used more than once** — a name prefix, an availability-zone slice, a CIDR carved with `cidrsubnet`, a boolean like `local.is_prod` that several `count` expressions read. 2. **A common tag map** merged into every resource, so adding a tag is a one-line edit. 3. **A readable name for an ugly expression**, so the resource block reads as intent rather than as string surgery. 4. **Building a map to drive `for_each`**, turning a list input into the keyed structure the resource needs. ## Choosing between the three Ask who needs to change the value. If a caller — a person, a CI pipeline, another module — must be able to change it, it is an input variable, and that makes it part of the module's public interface, which you then have to keep stable. If it is derived from other values and only this module cares, it is a local, and you can rename or restructure it freely because nothing outside depends on it. If something outside needs to *read* the result, it is an output. That asymmetry is the practical point: variables and outputs are interface, locals are implementation. Teams get into trouble by adding a variable with a default that nobody ever overrides (interface bloat) or by publishing every intermediate as an output "just in case" (a surface you can no longer change quietly). ## In a plan Locals are not resources, so they never appear as an entry in `terraform plan` output and are not recorded in state. They are evaluated as part of the dependency graph and their values simply show up wherever they are used. `terraform console` will evaluate `local.name_prefix` interactively, which is the fastest way to check what an expression is really producing.

  • Can a child module read a local defined in its parent?
    No. Locals are scoped to the module that declares them, and there is no `local.` reference across a module boundary. If a child needs the value, the parent must pass it as an argument in the `module` block, which means the child declares it as an input variable. This is deliberate: it keeps a module's interface explicit rather than letting children reach into their caller's internals.
  • If a value is derived but you also want callers to be able to override it, how do you model that?
    Declare an input variable that is optional — typically `default = null` — and a local that picks the caller's value when set and computes a fallback otherwise, for example `coalesce(var.name_prefix, "${var.project}-${var.environment}")`. Resources then read the local. The interface stays one variable, and the derivation stays in one place instead of being repeated at every use site.
  • Does renaming a local cause any resource changes on the next apply?
    No, as long as the values it produces are unchanged. Locals are not stored in state and are not tracked as objects, so Terraform only compares the final argument values it computes. Renaming `local.prefix` to `local.name_prefix` across the module yields an empty plan. Renaming an *output* is different — anything reading it, including other configurations via remote state, breaks.

saying these in an interview costs you the question

  • Claims a local can be overridden with -var at apply time
  • Writes locals.name instead of local.name in references
  • Says locals are constants that cannot depend on resources
  • Adds a variable with a default nobody overrides instead of a local
  • Exports every intermediate as an output just in case

context

open as a page

Your Terraform root module must stamp the same owner, environment and cost-centre tags on every AWS resource, plus a per-resource Name tag. How do you structure that with locals, and what happens on the next plan when someone adds a tag?

level: middleimportance: should knowfreq 62%

basics

~20 s

Define the shared tags once in a locals block as a map, then set each resource's tags to merge(local.common_tags, { Name = "..." }) so per-resource keys win. Adding a key is a one-line edit that produces an in-place tag update across every resource using the local.

open as a page

In Terraform, when are locals evaluated, and what shows up in the plan when a local's expression reads an attribute of a resource that does not exist yet?

level: middleimportance: should knowfreq 44%

basics

~20 s

Terraform evaluates locals as nodes in the dependency graph, not top to bottom, so declaration order does not matter. A local that reads an attribute of an uncreated resource is unknown during plan, and every value derived from it renders as (known after apply).

open as a page

You inherit a Terraform module where nearly every resource argument is a reference to a local, some chained three deep through other locals. What does that cost the team, and when is a local genuinely earning its place?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Every layer of indirection is another jump a reviewer must make to see the real value, and a widely-shared local silently fans one edit out across many resources. A local earns its place when it removes genuine repetition or names a complex expression, not when it merely renames a variable.

open as a page