| 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.
resource "terraform_data" "example" {
input = "development"
}Replace the literal with a declared input and a reference:
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.
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).
variable "environment" {
type = string
}With default, the variable is optional when no outer assignment exists:
variable "environment" {
type = string
default = "dev"
}Prepare the lab and declare variables
Create the working directory:
mkdir -p ~/terraform-labs/terraform-variables && cd ~/terraform-labs/terraform-variablesWrite variables.tf with several practical types:
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
}
EOFAdd a root module that references those inputs:
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
}
EOFInitialize the directory:
terraform init -input=falseSample 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:
echo 'var.environment' | terraform console"development"Check the numeric and boolean defaults:
echo 'var.instance_count' | terraform console1The boolean default should still be false:
echo 'var.enable_debug' | terraform consolefalseList values print as HCL collections. JSON-encoding makes multi-element lists easier to read in a transcript:
echo 'jsonencode(var.subnet_ids)' | terraform console"[\"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:
cat > terraform.tfvars <<'EOF'
environment = "staging"
instance_count = 2
EOFRun plan from the same directory. Terraform auto-loads terraform.tfvars without a flag:
terraform plan -input=false -no-colorLook 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:
printf 'environment = "test"\n' > aaa.auto.tfvars
printf 'environment = "prod"\n' > zzz.auto.tfvarsAsk console which value wins:
echo 'var.environment' | terraform console"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:
rm -f aaa.auto.tfvars zzz.auto.tfvarsCustom -var-file
Keep per-environment values in separate files without renaming the auto-loaded terraform.tfvars:
printf 'environment = "dev"\n' > dev.tfvarsPass the file explicitly. It overrides terraform.tfvars and *.auto.tfvars when both define the same variable:
echo 'var.environment' | terraform console -var-file=dev.tfvars"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:
echo 'var.environment' | terraform console -var='environment=staging'"staging"Quote carefully in Bash. Single quotes around the whole name=value pair prevent the shell from interpreting spaces or metacharacters:
terraform plan -var='environment=dev'TF_VAR environment variables
Remove terraform.tfvars temporarily so TF_VAR_* is not overridden by an auto-loaded file:
rm -f terraform.tfvarsExport TF_VAR_<variable_name> before running Terraform:
TF_VAR_environment=test bash -c 'echo var.environment | terraform console'"test"When you are done testing, clear the variable so later shells do not inherit a stale value:
unset TF_VAR_environmentTF_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:
cd ~/terraform-labs/terraform-variables
rm -f terraform.tfvars terraform.tfvars.json aaa.auto.tfvars zzz.auto.tfvars dev.tfvars
unset TF_VAR_environmentSave the main lab configuration before swapping in marker-only files for the ladder:
cp variables.tf variables.tf.main
cp main.tf main.tf.mainReplace main.tf with a minimal module that references only environment:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
output "environment" {
value = var.environment
}
EOFReplace 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:
cat > variables.tf <<'EOF'
variable "environment" {
type = string
default = "from-default"
}
EOFConfirm only the block default is active:
echo 'var.environment' | terraform console"from-default"Export TF_VAR_environment and read console again. TF_VAR_* beats the block default:
TF_VAR_environment=from-tfvar-env bash -c 'echo var.environment | terraform console'"from-tfvar-env"Create terraform.tfvars, which overrides TF_VAR_* when both are present:
printf 'environment = "from-tfvars"\n' > terraform.tfvars
TF_VAR_environment=from-tfvar-env bash -c 'echo var.environment | terraform console'"from-tfvars"Add terraform.tfvars.json. JSON auto-load files override plain terraform.tfvars:
printf '{"environment":"from-tfvars-json"}\n' > terraform.tfvars.json
echo 'var.environment' | terraform console"from-tfvars-json"Add auto-loaded files. Later lexical names win within that tier:
printf 'environment = "from-auto-early"\n' > aaa.auto.tfvars
printf 'environment = "from-auto-late"\n' > zzz.auto.tfvars
echo 'var.environment' | terraform console"from-auto-late"Pass -var-file=dev.tfvars on the command line. CLI flags beat every auto-loaded file:
printf 'environment = "from-varfile"\n' > dev.tfvars
echo 'var.environment' | terraform console -var-file=dev.tfvars"from-varfile"When both -var-file and -var appear, order matters. With -var-file first and -var second, the -var value wins:
echo 'var.environment' | terraform console -var-file=dev.tfvars -var='environment=from-cli-var'"from-cli-var"Reverse the flag order and the later -var-file wins instead:
echo 'var.environment' | terraform console -var='environment=from-cli-var' -var-file=dev.tfvars"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:
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_environmentValidation, 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:
terraform plan -input=false -no-color -var='environment=invalid-env'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:
echo 'var.optional_label' | terraform consoletostring(null)Sensitive and ephemeral variables
Mark credentials and tokens with sensitive = true so normal CLI output redacts them:
echo 'var.api_token' | terraform console(sensitive value)| Setting | Effect |
|---|---|
sensitive = true |
Redact the value in normal CLI operation output |
ephemeral = true |
Omit the value from state and plan files |
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.
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:
cp main.tf main.tf.main
cp variables.tf variables.tf.mainRequired variable with no value
Replace the module with a required variable and no default:
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.tfvarsDisable prompts to see the hard failure:
terraform plan -input=false -no-colorError: 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:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "environment" {
type = string
}
resource "terraform_data" "x" {
input = var.environment
}
EOFWith -input=true and a TTY, Terraform prompts instead of failing immediately. Run plan without piping stdin:
terraform plan -input=true -no-colorWhen Terraform prints var.environment and Enter a value:, type staging and press Enter:
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:
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.tfvarsPlan surfaces the type mismatch:
terraform plan -input=false -no-colorError: 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:
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.tfvarsRun plan to trigger the validation rule:
terraform plan -input=false -no-colorError: 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:
cat > terraform.tfvars <<'EOF'
environment = "dev"
broken =
EOFTerraform fails while parsing the broken assignment file:
terraform plan -input=false -no-colorError: 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:
rm -f terraform.tfvars
mv main.tf.main main.tf
mv variables.tf.main variables.tfQuick 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 avariable "foo"block inside your configuration.TF_VAR_foo— a process environment variable the Terraform CLI reads to populatevar.foobefore 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
- Input Variables — Terraform documentation
- Variable Definitions (
.tfvars) — Terraform documentation - Environment Variables — Terraform CLI
- terraform console command — Terraform CLI
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.

