Chapter 5: The Language — Variables, Outputs, Expressions
Topic 28

Input Variables

LanguageVariables

Input variables are the parameters of a Terraform configuration — the values you pull out of the body so the same code can stand up the Hatch ingest pipeline in dev, staging, and prod by changing inputs, not source. You declare a variable once with a variable "region" {} block, reference it everywhere as var.region, and supply its value through one of several channels that resolve in a fixed precedence order.

Until now the Hatch config carried "hatch-pipeline-dev" and "us-central1" as literals scattered through resource blocks. Parameterizing them is the first real step toward reuse: the .tf files become the logic, and a small set of inputs become the only thing that changes between environments. Getting the precedence order right — and knowing which variables to leave required — is what keeps a dev value from silently landing in prod.

Declaration and Reference

A variable block declares an input: it names it, gives it a type, and optionally a default and a description. Everywhere else in the configuration you write var.region, and Terraform substitutes the resolved value at plan time. The declaration is the only place the variable's shape is defined; every reference just reads it.

Declaring an input and referencing it
variable "region" {
  type        = string
  description = "GCP region for the Hatch pipeline's regional resources"
  default     = "us-central1"
}

resource "google_storage_bucket" "raw" {
  name     = "hatch-events-raw"
  location = var.region   # reads the resolved value at plan time
}

That is the whole mechanism: one declaration, any number of var. references. The variable has no value of its own until Terraform resolves it from the channels below, and a reference to an undeclared variable is an error — Terraform will not silently treat var.regon as a new input.

Defaults and Required Variables

A variable with a default is optional: the default applies whenever nothing overrides it. A variable with no default is required, and Terraform errors before it plans if the value is unset. That distinction is a deliberate design lever, not a formality — leaving off a default forces the caller to make a choice the configuration cannot safely make for them.

For the Hatch pipeline, region can reasonably default to us-central1 because applying to the wrong region is recoverable and cheap. The GCP project id cannot: a wrong project means resources land in the wrong place entirely. So project gets no default, and an apply without it fails loudly instead of guessing.

The .tfvars File

A .tfvars file holds the actual values for one environment. terraform.tfvars and any *.auto.tfvars are loaded automatically; a file named anything else — prod.tfvars, dev.tfvars — must be passed explicitly with -var-file. The canonical pattern is one .tfvars per environment selecting the project, region, and bucket names, while the .tf source stays byte-for-byte identical across all of them.

prod.tfvars — values for one environment
# applied with: terraform apply -var-file=prod.tfvars
project = "hatch-pipeline-prod"
region  = "us-central1"
raw_bucket_name = "hatch-events-raw-prod"

The same configuration applied with -var-file=dev.tfvars stands up the dev pipeline; with prod.tfvars, the prod one. The diff between environments lives entirely in those small files, which is exactly where a reviewer wants to see it.

The Precedence Order

When the same variable is set in more than one place, Terraform resolves it in a strict order, highest priority first: -var and -var-file flags on the command line, then auto-loaded *.auto.tfvars files (in lexical order), then terraform.tfvars, then TF_VAR_ environment variables, and finally the declared default. Within multiple -var-file flags, the last one on the command line wins. Knowing this order is what lets you predict which value an apply will actually use.

The subtle trap is the environment-variable tier. A stale TF_VAR_region exported in your shell sits just above the declared default — so if no .tfvars file and no -var flag sets region, the apply silently uses that env var instead of the default, and the diff gives no hint why. Reserve TF_VAR_ for CI injection and secrets, never for values that belong in a reviewable file.

Variable precedence — highest priority wins
-var / -var-file
*.auto.tfvars
terraform.tfvars
TF_VAR_ env
declared default

When a Variable Should Have No Default

Anything environment-specific and dangerous to guess wrong should be required. The GCP project id is the canonical example: give it a default of hatch-pipeline-dev and a prod apply with the project left unset will quietly create resources in the dev project instead of failing. A required variable converts that silent, expensive mistake into a loud pre-plan error. The same logic applies to prod bucket names and the environment name itself.

Descriptions and Self-Documentation

The description field is not decoration. It surfaces in interactive plan prompts when a required variable is unset, and it becomes the documentation for a module's inputs the moment the Hatch pipeline is packaged as one. On a shared module, the difference between a variable with a real description and one without is the difference between a usable input and a guessing game for the next engineer.

Variable vs Hardcoded Literal

Input variable — a value the caller supplies, resolved through the precedence chain. Use it for anything that differs between dev, staging, and prod: project, region, bucket names. The whole point is one config, many environments.

Hardcoded literal — a constant baked into a resource block. Fine for a value that genuinely never varies, but a literal that should differ between environments is the bug a variable exists to prevent. If you would ever change it per environment, it is an input.

Common Mistakes
  • Giving project a default like hatch-pipeline-dev and then running an apply meant for prod — the default silently lands every resource in the dev project instead of failing before plan.
  • Leaving a stale TF_VAR_region export in your shell while no .tfvars file or -var flag sets region — the env var outranks the declared default, and the apply uses a value nobody can see in the diff.
  • Naming a file prod.tfvars and expecting it to auto-load — only terraform.tfvars and *.auto.tfvars load automatically; prod.tfvars must be passed with -var-file or it is ignored.
  • Referencing var.regon with a typo — Terraform errors on the undeclared reference, which is at least loud, where a defaulted variable would have hidden the mistake.
  • Declaring a variable and never referencing it — Terraform stays silent, so the dead input lingers as noise that implies a knob that does nothing.
Best Practices
  • Leave off the default for any value unsafe to guess — project id, environment name, prod bucket names — so an unset input fails before plan instead of applying a dev default into prod.
  • Give every variable both a type and a description, since both surface in plan output and module documentation.
  • Use one *.tfvars file per environment and select it with -var-file, keeping the .tf source identical across dev, staging, and prod.
  • Reserve TF_VAR_ environment variables for secrets and CI injection, never for values that belong in a reviewable file, since env vars are invisible in the diff.
  • Keep environment-specific values in their .tfvars and out of resource blocks, so the source is the logic and the inputs are the only difference.
Comparable tools Pulumi config and per-stack config files Helm values.yaml per environment Ansible group_vars for environment values gcloud config profiles, the imperative analog for switching project and region

Knowledge Check

When the same variable is set in several places, which source wins?

  • A -var or -var-file flag on the command line outranks terraform.tfvars, which outranks a TF_VAR_ env var, which outranks the declared default
  • The declared default always wins because it is hard-coded inside the variable block itself
  • terraform.tfvars wins because Terraform loads it last and last write wins
  • Whichever source supplies the longest string value takes precedence over every shorter assignment that happens to target the same variable name

Why is leaving project required (no default) safer than giving it a default of hatch-pipeline-dev?

  • An unset value fails loudly before plan, instead of silently landing a prod apply in the dev project
  • Required variables apply faster because Terraform skips the default lookup on every single evaluation of the variable
  • A default on project would block the variable from being read as var.project elsewhere
  • A required variable is automatically marked sensitive and kept out of plan output

Which .tfvars files does Terraform load automatically?

  • terraform.tfvars and any *.auto.tfvars; others like prod.tfvars need -var-file
  • Every .tfvars file in the working directory, merged together in alphabetical order
  • Only the .tfvars file whose name matches the current workspace
  • None — every .tfvars file must be passed with an explicit -var-file

A teammate sets region in terraform.tfvars but the apply uses a different region. What is the most likely cause?

  • A -var flag or a *.auto.tfvars file sets region, and both outrank terraform.tfvars in the precedence order
  • A stale TF_VAR_region export left in their shell session silently outranks the region value in terraform.tfvars
  • The variable's declared default always overrides whatever the .tfvars file sets
  • The region argument cannot be driven by an input variable at all

You got correct