skip to content

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