In Terraform, the same input variable is set by the variable block's default, by terraform.tfvars, by an *.auto.tfvars file, by a TF_VAR_ environment variable and by -var on the command line. Which value does the run use, and what is the full precedence order?
answer
- six sources, last one wins
- environment variables are near the bottom
- command line always last
- auto files in lexical order
- complex values replaced, never merged
basics
~10 sThe command line wins. From lowest to highest, Terraform applies the variable block default, TF_VAR_ environment variables, terraform.tfvars, terraform.tfvars.json, *.auto.tfvars files in lexical order, then -var and -var-file options in the order given.
solid answer
~40 s`-var` wins, because command-line options are the last thing Terraform applies. The full order, lowest to highest, is: the `default` in the `variable` block, then `TF_VAR_` environment variables, then `terraform.tfvars`, then `terraform.tfvars.json`, then any `*.auto.tfvars` or `*.auto.tfvars.json` file in lexical filename order, and finally `-var` and `-var-file` options processed left to right, so the last one on the line wins. The part people get wrong is where the environment sits: `TF_VAR_` is the *weakest* real source, above only the default, so a committed `.auto.tfvars` file silently beats a value CI exported. Values are also replaced, not merged — since Terraform 0.12 a map or object from a higher tier overwrites the whole value rather than merging keys.
code
bash · 9 lines# variables.tf: variable "instance_count" { type = number; default = 1 }
# terraform.tfvars: instance_count = 2
# sizing.auto.tfvars: instance_count = 3
export TF_VAR_instance_count=4
terraform plan # -> 3 (auto.tfvars beats TF_VAR_)
terraform plan -var='instance_count=5' # -> 5 (command line wins)
terraform plan -var='instance_count=5' \
-var='instance_count=6' # -> 6 (last option wins)go deeper
Memorise the ordering and be able to say the command line wins over every file, and that a default only applies when nothing else supplied a value.
Explain each tier and why TF_VAR_ ranks below auto-loaded files, and state that a higher-precedence map replaces the whole value instead of merging keys.
Turn the order into a diagnosis: given a value that surprised someone, walk the sources in order and name the CI-versus-laptop difference that produced it.
Argue for a convention that removes the ambiguity — which tier each class of value is allowed to live in, and why ad-hoc -var overrides should be rare and recorded rather than routine.
## The ordered list Terraform collects a value for every root-module input variable from these sources, applying them in order so that a later source overrides an earlier one: 1. The `default` in the `variable` block — the fallback if nothing else supplies a value. 2. `TF_VAR_<name>` environment variables. 3. The `terraform.tfvars` file, if present. 4. The `terraform.tfvars.json` file, if present. 5. Any `*.auto.tfvars` or `*.auto.tfvars.json` files, in lexical order of their filenames. 6. Any `-var` and `-var-file` options on the command line, in the order they are given. If several `-var`/`-var-file` options touch the same variable, the rightmost one wins, because they are simply processed left to right. Variables set in an HCP Terraform (formerly Terraform Cloud) workspace arrive at that same top tier. ## The default is a fallback, not really a tier It helps to think of the `default` as what the variable is worth when no source assigned it, rather than as the first coat of paint. A variable declared without a default is not "empty": if nothing supplies a value, Terraform has no value at all and either prompts for one or fails. That distinction is what makes a defaultless variable a good way to force an environment to be named explicitly. ## The environment-variable surprise The single most-missed fact is that `TF_VAR_` sits at the **bottom** of the real sources. It is described in the documentation as a fallback for the other ways of defining variables. Intuition says the opposite — everywhere else in Unix tooling an environment variable is an override — and that intuition is what produces the classic incident: a pipeline exports `TF_VAR_app_version` from its release metadata, someone commits a `dev.auto.tfvars` that also pins `app_version`, and the pipeline's value stops taking effect with no error anywhere. ```bash export TF_VAR_region=eu-west-1 # loses... cat region.auto.tfvars # ...to this # region = "us-east-1" ``` Terraform converts the string in the environment variable to the variable's declared type, so a list can be supplied as `TF_VAR_zones='["eu-west-1a","eu-west-1b"]'`. Names are case-sensitive and match the declared variable name exactly. ## Command-line options are ordered among themselves ```bash terraform plan \ -var-file=common.tfvars \ -var-file=prod.tfvars \ -var='instance_count=6' ``` This is a deliberate layering idiom: shared values first, environment values next, one-off overrides last. It also means an operator can always win an argument with the repository, which is convenient in an incident and dangerous as a habit — nothing about a `-var` override is reviewable afterwards unless the job log records the command. ## No merging for complex types Since Terraform 0.12, a map or object variable behaves like any other: the highest-precedence source replaces the entire value. Setting `tags = { team = "payments" }` in `terraform.tfvars` and `tags = { env = "prod" }` via `-var-file` gives you `{ env = "prod" }`, not the union. If you want merging, do it in the configuration — a `locals` expression that merges a common map with a per-environment map — rather than expecting the loader to do it. ## Working out which value actually won There is no built-in "explain this variable" command, so you bisect the list in order: - `env | grep TF_VAR_` in the exact shell or job that runs Terraform. - List the directory for `terraform.tfvars*` and `*.auto.tfvars*` — remembering that `-chdir` changes which directory that is. - Read the full Terraform command line from the CI job log, including options injected by a wrapper script. A temporary `output` echoing the value, or `terraform console` in the same directory, confirms what the configuration actually resolved. Note that a value assigned to a variable the root module does not declare produces a warning about a value for an undeclared variable and is then ignored — so a misspelled name in a tfvars file looks exactly like a precedence problem. ## Why interviewers ask this Because it is the mechanism behind "it works on my laptop but not in CI", and its inverse. A candidate who can recite the order and then say *why* the environment tier being low matters has shown they have debugged a real pipeline, not just read the documentation page.
- Two -var-file options set the same map variable with different keys. Does Terraform merge them?No. Since Terraform 0.12, map and object variables follow the same rule as everything else: the higher-precedence source replaces the whole value, so you get the map from the later `-var-file` and nothing from the earlier one. If you need a union, merge explicitly in the configuration — for example a `locals` value built with `merge(var.common_tags, var.env_tags)` — so the merge is visible in code and in the plan.
- Can a tfvars file or TF_VAR_ set a variable declared inside a child module?No. All of these sources feed the root module's input variables only. A child module's variables are set exclusively by arguments in the `module` block that calls it, so the usual pattern is a root variable that is passed down explicitly. This is why adding a knob deep in a module tree means threading a variable through every level, and why people reach for shared `locals` or an object variable to avoid a long chain of pass-throughs.
- How would you find out which source supplied a value that surprised you?There is no built-in provenance report, so you walk the order: check `env | grep TF_VAR_` in the shell that actually runs Terraform, list the directory for `terraform.tfvars*` and `*.auto.tfvars*`, then read the full command line from the CI log including anything a wrapper script adds. `terraform console` or a temporary output confirms the resolved value. Remember `-chdir` changes which directory the auto-loaded files come from.
Think of the sources as coats of paint applied in a fixed order: each later coat covers whatever is underneath, and the command line is always the last coat to go on.
saying these in an interview costs you the question
- Says environment variables override tfvars files
- Thinks the default is applied last as a safety net
- Believes maps from different sources are merged together
- Assumes the first -var on the line wins rather than the last
- Thinks tfvars values reach variables declared in child modules