7 — Variables and locals¶
Assumes: Dependencies and the graph.
Two different jobs, often confused because both hold values.
variable |
locals |
|
|---|---|---|
| Analogy | a function argument | a local variable inside the function |
| Set from outside? | yes — CLI, .tfvars, env var, default |
never |
| Referenced as | var.name |
local.name |
| Purpose | what changes per run/user/environment | what's derived once and reused |
Which to use: something that legitimately changes per run, user or environment (project
ID, target environment name) → variable. Something computed from other inputs and reused
throughout the config (a name prefix, a labels map, a prod-vs-nonprod flag) → local.
Naming trap: the block is locals (plural), the reference is local. (singular).
Declaring a variable¶
variable "project_id" {
description = "The GCP project to create resources in"
type = string
default = "my-devops-journey-502420"
}
description is not decoration — it appears in terraform plan prompts and in generated
module documentation, and it's the only place a caller of a
module learns what the input means.
Type constraints¶
| Type | Example value | Notes |
|---|---|---|
string, number, bool |
"us-central1", 3, true |
primitives |
list(string) |
["a", "b"] |
ordered, duplicates allowed — what count wants |
set(string) |
["a", "b"] |
unordered, unique — what for_each wants |
map(string) |
{ env = "dev" } |
keyed |
object({...}) |
{ id = string, size = number } |
named fields, optional() for defaults |
any |
anything | opts out of checking; use sparingly |
The list vs set distinction is not pedantry — it decides whether
count or for_each is available to you.
Variable precedence¶
Highest priority first. Anything higher overrides anything lower for the same variable.
- CLI flags —
-var="project_id=..."or-var-file="..." *.auto.tfvars— loaded automatically, in alphabetical order; among several, the alphabetically last wins for any overlapping variableterraform.tfvars(or.tfvars.json) — the standard project variable file- Environment variables —
TF_VAR_<name>, e.g.TF_VAR_project_id defaultin thevariableblock — used only if nothing above supplies a value
Three points that are commonly guessed wrong:
- File-based inputs (
terraform.tfvars) beatTF_VAR_*environment variables — files are treated as more explicit and more project-specific. *.auto.tfvarsbeats plainterraform.tfvars.- Among multiple
*.auto.tfvarsfiles, alphabetically last wins.
A variable with no default and no supplied value is not an error at parse time — Terraform interactively prompts for it, which is why a CI run can appear to hang instead of failing.
validation — reject bad input early¶
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of dev, staging, prod."
}
}
The condition is evaluated at plan time, before any API call. A typo becomes a clear
message instead of a half-created environment named prodd. The condition may only
reference the variable being validated — that restriction is what lets Terraform check it
so early.
sensitive inputs¶
sensitive = true masks the value in CLI output, and taints anything derived from it so
that plans don't leak it indirectly. It does not encrypt anything, and the plaintext
still lands in the state file. It's protection against
shoulder-surfing and CI logs, not a security boundary.
Volatile locals cause perpetual diffs¶
timestamp() re-evaluates on every plan, so any resource referencing a value derived from
it is always different and can never converge. When you genuinely need one, the standard
fix is ignore_changes on the affected attribute.
Key takeaway¶
variable is the config's input schema; locals is its internal wiring. Most
"why is my value not what I set" incidents are precedence, and the answer is almost always
that a .tfvars file is quietly winning.
Labs: Variables, Locals, Validation and sensitive values, Lab 8, Lab 15 · Quiz: Variables, expressions & guards · Next: Expressions and conditionals