Terraform Variables: Types, tfvars and Precedence

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user; sudo only if Terraform is not installed yet
Scope Terraform input variable blocks, types, required vs default, terraform.tfvars, terraform.tfvars.json, *.auto.tfvars, -var-file, -var, TF_VAR, CLI variable precedence, validation, nullable, sensitive, and common variable errors. Does not cover full type theory, outputs, locals depth, HCP Terraform variable sets, Vault, or module input design.
Related guides Terraform resources
Terraform HCL syntax
terraform plan command
Terraform data sources
Terraform Associate certification course

Hardcoding every environment name inside a Terraform resource block works for a quick demo, but it does not scale. You want one configuration directory that accepts different values per workspace, pipeline stage, or operator without editing .tf files each time.

hcl
resource "terraform_data" "example" {
  input = "development"
}

Replace the literal with a declared input and a reference:

hcl
variable "environment" {
  type    = string
  default = "development"
}

resource "terraform_data" "example" {
  input = var.environment
}

Now environment is an input variable. You declare it once, then supply values through defaults, variable files, flags, or TF_VAR_* environment variables.

Every command in this walkthrough runs in one working directory, ~/terraform-labs/terraform-variables/. You initialize the lab once, then create or overwrite assignment files and configuration snippets with heredocs as each section needs them.

NOTE
Use the Terraform lab environment on Ubuntu. Run terraform init once in the lab directory before plan or console. Examples use the built-in terraform_data resource so you do not need cloud credentials.

Variable block syntax and Terraform variable types

A variable block defines a named input the root module (or a child module) accepts. Terraform evaluates the block at configuration load time, before terraform plan builds the graph.

Argument Purpose
type Constraint on accepted values (string, number, bool, and collection or structural types such as list, set, map, object, and tuple)
default Value used when nothing else supplies the variable; omit to make it required
description Human-readable documentation shown in prompts and some tooling
validation Custom condition / error_message rules evaluated after type checking
sensitive Redacts the value in normal CLI operation output
nullable Controls whether callers may explicitly assign null; default is true
ephemeral Omits the variable value from state and plan files (advanced; not exercised here)

Inside object type constraints, optional(...) marks attributes that callers may omit. That is separate from the nullable argument on the variable block itself.

A useful description tells the reader what the value controls and any allowed values. Avoid placeholders like “This is the environment variable.”

Required vs optional variables

Without default, Terraform treats the variable as required. You must supply a value through another mechanism or Terraform stops with an error (or prompts interactively).

hcl
variable "environment" {
  type = string
}

With default, the variable is optional when no outer assignment exists:

hcl
variable "environment" {
  type    = string
  default = "dev"
}

Prepare the lab and declare variables

Create the working directory:

bash
mkdir -p ~/terraform-labs/terraform-variables && cd ~/terraform-labs/terraform-variables

Write variables.tf with several practical types:

bash
cat > variables.tf <<'EOF'
variable "environment" {
  type        = string
  description = "Deployment environment label used in resource tags"
  default     = "development"

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

variable "instance_count" {
  type    = number
  default = 1
}

variable "enable_debug" {
  type    = bool
  default = false
}

variable "subnet_ids" {
  type    = list(string)
  default = ["subnet-a", "subnet-b"]
}

variable "api_token" {
  type      = string
  sensitive = true
  default   = "changeme"
}

variable "optional_label" {
  type     = string
  nullable = true
  default  = null
}
EOF

Add a root module that references those inputs:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "example" {
  input = var.environment
}

output "environment" {
  value = var.environment
}

output "instance_count" {
  value = var.instance_count
}

output "enable_debug" {
  value = var.enable_debug
}

output "subnet_ids" {
  value = var.subnet_ids
}

output "api_token" {
  value     = var.api_token
  sensitive = true
}

output "optional_label" {
  value = var.optional_label
}
EOF

Initialize the directory:

bash
terraform init -input=false

Sample output:

output
Terraform has been successfully initialized!

Inspect loaded values with terraform console. The block default is active because no file or flag overrides environment yet:

bash
echo 'var.environment' | terraform console
output
"development"

Check the numeric and boolean defaults:

bash
echo 'var.instance_count' | terraform console
output
1

The boolean default should still be false:

bash
echo 'var.enable_debug' | terraform console
output
false

List values print as HCL collections. JSON-encoding makes multi-element lists easier to read in a transcript:

bash
echo 'jsonencode(var.subnet_ids)' | terraform console
output
"[\"subnet-a\",\"subnet-b\"]"

For full constraint semantics (set vs list, object shapes, optional(...) attributes), see the dedicated Terraform data types lesson. This page focuses on how you declare variables and feed values into them.


Assign Terraform variables with tfvars, flags, and TF_VAR

Terraform loads variable values from several sources. Variable files keep environment-specific values separate from .tf configuration. If a .tfvars file contains secrets, protect it and keep it out of version control.

terraform.tfvars

Create terraform.tfvars beside your configuration:

bash
cat > terraform.tfvars <<'EOF'
environment    = "staging"
instance_count = 2
EOF

Run plan from the same directory. Terraform auto-loads terraform.tfvars without a flag:

bash
terraform plan -input=false -no-color

Look for environment = "staging" and instance_count = 2 in the proposed output changes. Both values pass validation because staging is in the allowed set.

*.auto.tfvars

Any file ending in .auto.tfvars or .auto.tfvars.json is loaded automatically, in lexical file name order. Later files override earlier ones for the same variable.

Add two auto-loaded files with values that pass validation:

bash
printf 'environment = "test"\n' > aaa.auto.tfvars
printf 'environment = "prod"\n' > zzz.auto.tfvars

Ask console which value wins:

bash
echo 'var.environment' | terraform console
output
"prod"

Alphabetical load order means zzz.auto.tfvars beats aaa.auto.tfvars.

Remove the auto files before the precedence walkthrough so they do not skew later steps:

bash
rm -f aaa.auto.tfvars zzz.auto.tfvars

Custom -var-file

Keep per-environment values in separate files without renaming the auto-loaded terraform.tfvars:

bash
printf 'environment = "dev"\n' > dev.tfvars

Pass the file explicitly. It overrides terraform.tfvars and *.auto.tfvars when both define the same variable:

bash
echo 'var.environment' | terraform console -var-file=dev.tfvars
output
"dev"

Use test.tfvars or prod.tfvars the same way in CI jobs. This article stops at file mechanics; it does not prescribe a full environment architecture.

-var on the command line

For one-off overrides, pass name=value pairs:

bash
echo 'var.environment' | terraform console -var='environment=staging'
output
"staging"

Quote carefully in Bash. Single quotes around the whole name=value pair prevent the shell from interpreting spaces or metacharacters:

bash
terraform plan -var='environment=dev'

TF_VAR environment variables

Remove terraform.tfvars temporarily so TF_VAR_* is not overridden by an auto-loaded file:

bash
rm -f terraform.tfvars

Export TF_VAR_<variable_name> before running Terraform:

bash
TF_VAR_environment=test bash -c 'echo var.environment | terraform console'
output
"test"

When you are done testing, clear the variable so later shells do not inherit a stale value:

bash
unset TF_VAR_environment

TF_VAR_* names map to root-module input variables. They are not a separate Terraform language feature; they are one more way to populate var.<name>.


Terraform variable precedence on the CLI

When the same variable is set in multiple places, Terraform picks a single winner. HashiCorp documents precedence for the Terraform CLI from lowest to highest:

Priority Source
1 default in the variable block
2 TF_VAR_<name> environment variable
3 terraform.tfvars
4 terraform.tfvars.json
5 *.auto.tfvars / *.auto.tfvars.json (later file name wins)
6 -var and -var-file on the command line, in the order provided

-var and -var-file share the top CLI tier. The later conflicting option on the command line wins — not -var unconditionally.

Clean assignment files from earlier sections so you start from a known baseline:

bash
cd ~/terraform-labs/terraform-variables
rm -f terraform.tfvars terraform.tfvars.json aaa.auto.tfvars zzz.auto.tfvars dev.tfvars
unset TF_VAR_environment

Save the main lab configuration before swapping in marker-only files for the ladder:

bash
cp variables.tf variables.tf.main
cp main.tf main.tf.main

Replace main.tf with a minimal module that references only environment:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

output "environment" {
  value = var.environment
}
EOF

Replace variables.tf with a single marker variable for the ladder. Each marker value is unique so you can see which layer won. Validation is omitted here so marker strings do not conflict with the main lab rule:

bash
cat > variables.tf <<'EOF'
variable "environment" {
  type    = string
  default = "from-default"
}
EOF

Confirm only the block default is active:

bash
echo 'var.environment' | terraform console
output
"from-default"

Export TF_VAR_environment and read console again. TF_VAR_* beats the block default:

bash
TF_VAR_environment=from-tfvar-env bash -c 'echo var.environment | terraform console'
output
"from-tfvar-env"

Create terraform.tfvars, which overrides TF_VAR_* when both are present:

bash
printf 'environment = "from-tfvars"\n' > terraform.tfvars
TF_VAR_environment=from-tfvar-env bash -c 'echo var.environment | terraform console'
output
"from-tfvars"

Add terraform.tfvars.json. JSON auto-load files override plain terraform.tfvars:

bash
printf '{"environment":"from-tfvars-json"}\n' > terraform.tfvars.json
echo 'var.environment' | terraform console
output
"from-tfvars-json"

Add auto-loaded files. Later lexical names win within that tier:

bash
printf 'environment = "from-auto-early"\n' > aaa.auto.tfvars
printf 'environment = "from-auto-late"\n' > zzz.auto.tfvars
echo 'var.environment' | terraform console
output
"from-auto-late"

Pass -var-file=dev.tfvars on the command line. CLI flags beat every auto-loaded file:

bash
printf 'environment = "from-varfile"\n' > dev.tfvars
echo 'var.environment' | terraform console -var-file=dev.tfvars
output
"from-varfile"

When both -var-file and -var appear, order matters. With -var-file first and -var second, the -var value wins:

bash
echo 'var.environment' | terraform console -var-file=dev.tfvars -var='environment=from-cli-var'
output
"from-cli-var"

Reverse the flag order and the later -var-file wins instead:

bash
echo 'var.environment' | terraform console -var='environment=from-cli-var' -var-file=dev.tfvars
output
"from-varfile"

That reversal is the detail many precedence cheat sheets omit. -var is not permanently above -var-file; whichever CLI assignment appears last takes precedence.

Restore the main lab configuration before validation and error demos:

bash
mv variables.tf.main variables.tf
mv main.tf.main main.tf
rm -f terraform.tfvars terraform.tfvars.json aaa.auto.tfvars zzz.auto.tfvars dev.tfvars
unset TF_VAR_environment
IMPORTANT
This precedence table applies to the Terraform CLI with local operations. HCP Terraform and Terraform Enterprise add workspace variables, variable sets, and run-task context that follow a different ordering. Treat remote backends as a separate topic.

Validation, nullable, and sensitive variables

Custom validation rules

Add a validation block when allowed values are narrower than the type allows. The environment variable in the restored variables.tf already includes one.

Supply a value outside that set and plan fails before any resource change:

bash
terraform plan -input=false -no-color -var='environment=invalid-env'
output
Error: Invalid value for variable

  on <value for var.environment>:
   1: environment = "invalid-env"
    ├────────────────
    │ var.environment is "invalid-env"

environment must be dev, test, prod, development, or staging.

This was checked by the validation rule at variables.tf:6,3-13.

For condition style comparisons across resources, see the Terraform validation and check blocks lesson.

Nullable variables

nullable controls whether callers may explicitly assign null. Its default is true.

When nullable = true and the variable has a non-null default, explicitly passing null overrides that default with null.

The lab leaves optional_label nullable with default = null. Console shows how Terraform prints that:

bash
echo 'var.optional_label' | terraform console
output
tostring(null)

Sensitive and ephemeral variables

Mark credentials and tokens with sensitive = true so normal CLI output redacts them:

bash
echo 'var.api_token' | terraform console
output
(sensitive value)
Setting Effect
sensitive = true Redact the value in normal CLI operation output
ephemeral = true Omit the value from state and plan files
WARNING
sensitive = true redacts display in normal CLI output; it does not remove values from Terraform state or saved plan files. Anyone with state access can still read stored sensitive values. For handling secrets in persisted artifacts, see Terraform sensitive data.

Refer to variables and common variable errors

Use var.<name> in expressions such as resource arguments, data-source arguments, locals, outputs, module arguments, and supported conditions. A variable default must be a literal value and cannot reference other configuration objects, including other input variables.

hcl
resource "terraform_data" "example" {
  input = var.environment
}

output "environment" {
  value = var.environment
}

The subsections below overwrite configuration files in the same lab directory. Run terraform init only if you removed .terraform/; the provider install from the start of the article is enough otherwise.

Save the main lab configuration before the error demos so you can restore it afterward:

bash
cp main.tf main.tf.main
cp variables.tf variables.tf.main

Required variable with no value

Replace the module with a required variable and no default:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
variable "environment" {
  type = string
}
resource "terraform_data" "x" {
  input = var.environment
}
output "environment" {
  value = var.environment
}
EOF
rm -f variables.tf terraform.tfvars

Disable prompts to see the hard failure:

bash
terraform plan -input=false -no-color
output
Error: No value for required variable

  on main.tf line 4:
   4: variable "environment" { type = string }

The root module input variable "environment" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.

Restore a prompt-friendly module for the interactive case:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
variable "environment" {
  type = string
}
resource "terraform_data" "x" {
  input = var.environment
}
EOF

With -input=true and a TTY, Terraform prompts instead of failing immediately. Run plan without piping stdin:

bash
terraform plan -input=true -no-color

When Terraform prints var.environment and Enter a value:, type staging and press Enter:

output
var.environment
  Enter a value: staging

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # terraform_data.x will be created
  + resource "terraform_data" "x" {
      + id     = (known after apply)
      + input  = "staging"
      + output = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Wrong type in a tfvars file

Restore variables.tf and add a numeric variable, then break the assignment file:

bash
cat > variables.tf <<'EOF'
variable "instance_count" {
  type = number
}
EOF
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
  input = tostring(var.instance_count)
}
EOF
printf 'instance_count = "two"\n' > terraform.tfvars

Plan surfaces the type mismatch:

bash
terraform plan -input=false -no-color
output
Error: Invalid value for input variable

  on terraform.tfvars line 1:
   1: instance_count = "two"

The given value is not suitable for var.instance_count declared at
variables.tf:1,1-26: a number is required.

Validation failure from tfvars

Validation runs after type checking, so a well-typed but disallowed value still fails plan:

bash
cat > variables.tf <<'EOF'
variable "environment" {
  type = string
  validation {
    condition     = var.environment == "prod"
    error_message = "environment must be prod."
  }
}
EOF
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
  input = var.environment
}
EOF
printf 'environment = "dev"\n' > terraform.tfvars

Run plan to trigger the validation rule:

bash
terraform plan -input=false -no-color
output
Error: Invalid value for variable

  on terraform.tfvars line 1:
   1: environment = "dev"
    ├────────────────
    │ var.environment is "dev"

environment must be prod.

This was checked by the validation rule at variables.tf:4,3-13.

Malformed tfvars syntax

A typo in the assignment file fails during load:

bash
cat > terraform.tfvars <<'EOF'
environment = "dev"
broken =
EOF

Terraform fails while parsing the broken assignment file:

bash
terraform plan -input=false -no-color
output
Error: Invalid expression

  on terraform.tfvars line 2:
   2: broken =

Expected the start of an expression, but found an invalid expression token.

Restore the main lab configuration before the locals section:

bash
rm -f terraform.tfvars
mv main.tf.main main.tf
mv variables.tf.main variables.tf

Quick troubleshooting reference

Symptom Likely cause Fix
No value for required variable Required input with -input=false and no assignment Pass -var, -var-file, terraform.tfvars, or TF_VAR_*; or add default
a number is required / type errors tfvars or -var value does not match type Fix the literal or widen the type constraint
Invalid value for variable validation condition returned false Use an allowed value or relax the rule
Invalid expression in tfvars Syntax error in the var file Fix HCL quoting and = spacing
Shell ate part of -var Missing quotes around name=value Wrap the pair in single quotes
Unexpected value at plan time Another assignment source wins on precedence Check auto.tfvars, TF_VAR_*, CLI flag order, and CI flags

Variables vs locals and TF_VAR naming

Input variables cross module boundaries. Callers pass them in; the module exposes them in variable blocks. Locals (local.<name>) are computed inside a module and are not set from the CLI.

Input variable Local value
Set from CLI / tfvars Yes No
Syntax var.environment local.name_prefix
Typical use Environment name, region, feature flags Derived names, tags, repeated expressions

Do not confuse Terraform input variables with Unix environment variables:

  • var.foo — a value declared in a variable "foo" block inside your configuration.
  • TF_VAR_foo — a process environment variable the Terraform CLI reads to populate var.foo before evaluation.

Only the TF_VAR_ prefix connects the shell environment to Terraform input variables. Other env vars (for example provider credentials) follow provider-specific rules and are not var.* references.


References


Summary

You started from a hardcoded resource argument and replaced it with a variable block plus var.environment. That pattern is how one Terraform root module serves multiple environments without forking source code.

Along the way you declared practical types, marked secrets sensitive, allowed null with nullable, and rejected bad input with validation. File-based workflows — terraform.tfvars, terraform.tfvars.json, *.auto.tfvars, and custom -var-file paths — keep per-environment values out of .tf files, while -var and TF_VAR_* cover ad hoc and pipeline injection.

The precedence ladder on Terraform 1.15.8 is worth memorizing in outline: defaults sit at the bottom, then TF_VAR_*, terraform.tfvars, terraform.tfvars.json, auto-loaded var files, and finally -var / -var-file processed in command-line order at the top. If a plan shows a surprising value, walk that list — and check whether a later CLI flag overrode an earlier one — before editing configuration.

Next, deepen type constraints in Terraform data types, or read how Terraform locals derive values from the inputs you set here.


Frequently Asked Questions

1. What is the difference between terraform.tfvars and a custom -var-file?

Both are HCL variable assignment files. terraform.tfvars is loaded automatically when present in the working directory. A path you pass with -var-file is loaded on demand and overrides values from terraform.tfvars and auto.tfvars files when both define the same variable.

2. Does TF_VAR beat terraform.tfvars?

No. On the Terraform CLI, terraform.tfvars overrides TF_VAR when both set the same variable. terraform.tfvars.json overrides terraform.tfvars, and auto-loaded var files sit above both.

3. What happens if I omit a required Terraform variable?

With -input=false, plan or apply fails with No value for required variable. With -input=true and a TTY, Terraform prompts Enter a value for each unset required variable.

4. Does sensitive = true keep a variable out of state?

No. sensitive causes Terraform to redact the value in normal CLI operation output, but the value is still stored in state and plan files. Use ephemeral variables when you need values omitted from persisted artifacts.

5. Which assignment method has the highest precedence on the Terraform CLI?

-var and -var-file share the highest CLI tier. Terraform processes them in the order they appear on the command line, and the later conflicting option wins. Below that tier come auto.tfvars files, then terraform.tfvars.json, terraform.tfvars, TF_VAR, and the variable default.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)