Six sources of values
Terraform reads variable values from several places. From highest to lowest priority:
-varon the CLI,terraform plan -var="env=prod". Overrides everything.-var-fileon the CLI,terraform plan -var-file=prod.tfvars. When multiple-var-fileflags are present, the last one wins.*.auto.tfvarsand*.auto.tfvars.json, all files with this suffix in the project root are loaded automatically. They are processed in alphabetical order; the last one alphabetically wins on a collision.terraform.tfvarsandterraform.tfvars.json, a fixed name, loaded automatically.TF_VAR_*environment variables,export TF_VAR_env=prodthenterraform plan.defaultin the variable block. The lowest priority, a fallback.
If none of the above provides a value and there is no default, Terraform prompts interactively in the terminal.
Priority example
Suppose a variable is declared as:
variable "env" {type = string
default = "dev"
}
What Terraform sees in different situations:
| Sources | Value of var.env |
|---|---|
| Nothing | "dev" (from default) |
export TF_VAR_env=staging | "staging" |
TF_VAR_env=staging + terraform.tfvars contains env = "prod" | "prod" (.tfvars overrides the env var) |
All of the above + -var-file=temp.tfvars where env = "qa" | "qa" |
All of the above + -var="env=hotfix" | "hotfix" |
The rule is simple: closer to the CLI wins.
When to use each source
default, for values that rarely change and cover 90% of cases. AWS region, small instance type.terraform.tfvars, the main values for the project. Often added to.gitignorebecause it may contain tokens and secrets.*.auto.tfvars, per-environment configuration when you are not using-var-file. For example,dev.auto.tfvarsloads automatically in the dev directory.-var-file, for CI/CD: one HCL config, separate tfvars for dev/staging/prod. The file is selected by an environment variable or a flag.-var, for one-off overrides. "Run plan once with a different value without touching any files."TF_VAR_*, for CI: secrets that must not go into git. The CI system injects them as environment variables.
TF_VAR_*, specifics
The prefix TF_VAR_ followed by the variable name:
export TF_VAR_env=prod
export TF_VAR_db_password="$(vault read -field=password kv/prod/db)"
terraform plan
For a variable of type string, number, or bool, the env value is used as-is. For type list or map, the env value must be JSON:
export TF_VAR_tags='{"env": "prod", "team": "platform"}'export TF_VAR_azs='["us-east-1a", "us-east-1b"]'
Case matters: TF_VAR_my_var is not the same as TF_VAR_MY_VAR.
auto.tfvars vs .tfvars, a subtle difference
terraform.tfvars is the only fixed name.
*.auto.tfvars files can have any name with that suffix.
Why the split?
terraform.tfvarsis the primary set; often added to .gitignore.dev.auto.tfvars,staging.auto.tfvars,prod.auto.tfvarshold environment values and are usually committed. All of them load at once, so a single workspace should contain only the one you need.
An alternative approach: keep environment files in separate directories without the auto suffix, and pass them with -var-file=envs/prod.tfvars. This is simpler for CI.
Pitfalls
-
All
*.auto.tfvarsfiles load. If your project root has bothdev.auto.tfvarsandprod.auto.tfvarsat the same time, both are read; the last one alphabetically wins. This is often not what you want. -
terraform.tfvarsin git is a common secret leak. If you store tokens there, add it to.gitignore. Do not rely on "nobody will look." -
Do not pass secrets via
-var. They end up in shell history. UseTF_VAR_*withread -sor pull them from a secrets manager. -
JSON is required for complex types in env vars. If you declared
type = list(string)and setTF_VAR_x=us-east-1, Terraform does not parse this as a list and will fail. You needTF_VAR_x='["us-east-1"]'. -
Interactive input does not work in CI. If a pipeline forgets to pass a variable, Terraform will wait for keyboard input and the job will hang indefinitely. Fix this with
-input=false, which makes Terraform fail with a clear error instead of waiting.