Variables, Outputs & Locals

Terraform / Infrastructure as Code

Chapter 4 · Variables, Outputs & Locals

Every value so far has been hardcoded directly into a resource block. Real configurations need to be parameterized — the same configuration deployed to dev, staging, and prod, with only a handful of values actually differing between them.

Input Variables

A variable block declares a named input, with an optional type, default, and description. Reference it anywhere in the configuration with var.<name>.

variable "instance_count" { type = number default = 2 description = "How many instances to create" } resource "local_file" "config" { for_each = toset(range(var.instance_count)) filename = "${path.module}/instance-${each.key}.txt" }

Omitting default makes the variable required — Terraform refuses to plan until a value is supplied, useful for forcing an explicit choice (like an environment name) rather than silently falling back to a guess.

Variable Validation

A validation block rejects bad input at plan time, before anything is ever created — cheaper than discovering the mistake mid-apply.

variable "environment" { type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be dev, staging, or prod." } }

Sensitive Variables

Marking a variable sensitive = true redacts its value from CLI plan/apply output — Terraform prints (sensitive value) instead of the real string.

variable "db_password" { type = string sensitive = true }
sensitive hides output, not the state file
sensitive = true only redacts CLI output — the real value is still written into the state file in plaintext. Protecting that file is Chapter 5's own topic; marking a variable sensitive is not, by itself, a complete solution to keeping a secret safe.

Locals

A locals block defines internal computed values — not inputs, just names for expressions used more than once. Unlike variables, locals aren't set from outside the configuration; they exist purely to avoid repeating the same expression.

locals { name_prefix = "${var.environment}-${var.project}" } resource "local_file" "config" { filename = "${path.module}/${local.name_prefix}-config.txt" }

.tfvars Files

Instead of typing -var flags on every run, values can live in a .tfvars file. terraform.tfvars and any *.auto.tfvars file load automatically; anything else needs an explicit -var-file flag — the pattern Chapter 8's workspaces build on for genuinely separate dev.tfvars/prod.tfvars files per environment.

# terraform.tfvars environment = "dev" instance_count = 2

Variable Precedence Order

Lowest to highest priority — later sources override earlier ones:

  1. Environment variables (TF_VAR_name)
  2. terraform.tfvars, if present
  3. terraform.tfvars.json, if present
  4. Any *.auto.tfvars/*.auto.tfvars.json files, in alphabetical order
  5. -var and -var-file command-line flags, in the order given — always win
CLI flags always win
A -var flag on the command line overrides every file-based source — useful for one-off overrides during testing, but easy to forget about when a plan doesn't match what the .tfvars file seems to say.

Hands-On Exercises

Exercise 1

Explain why sensitive = true alone is not sufficient to protect a secret value, and name what else actually needs protecting.

📄 View solution
Exercise 2

terraform.tfvars sets region = "us-east-1", and the run also passes -var="region=us-west-2" on the command line. Which value does Terraform actually use, and why?

📄 View solution
Exercise 3

Explain the practical difference between a variable and a local, and describe a scenario where each is the right choice.

📄 View solution

Chapter 4 Quick Reference

  • variable — an input; omit default to make it required
  • validation — rejects bad input at plan time
  • sensitive = true — redacts CLI output only, not the state file
  • locals — internal computed values, not external inputs
  • Precedence (low → high) — env vars → terraform.tfvars*.auto.tfvars (alphabetical) → -var/-var-file flags