Variables are how a configuration stops describing one environment and starts describing any environment. They are also where a specific class of bug hides: Terraform will quietly coerce types for you unless you tell it not to.
Topic 1: Declaring Inputs
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
}
variable "db_password" {
type = string
sensitive = true
}
A variable with no default is required. If nothing supplies it, Terraform prompts interactively and prints the description as the prompt — the practical reason to write descriptions even for internal variables.
Reference them as var.<name>.
Topic 2: The Type System
| Type | Behaviour |
|---|---|
string, number, bool | Primitives |
list(TYPE) | Ordered, duplicates allowed, zero-indexed — var.subnets[0] |
set(TYPE) | Unordered, duplicates silently removed |
map(TYPE) | Key-value, accessed by key — var.amis["eu-west-1"] |
tuple([TYPE, ...]) | Fixed length, per-position types |
object({ k = TYPE }) | Named fields, arbitrarily nestable |
any | Placeholder; type inferred at runtime from the value |
Set behaviour catches people out. Declaring set(number) with [7, 2, 2] gives you [7, 2] — the duplicate is gone, no warning:
toset(["foo", "bar", "foo"]) # -> ["bar", "foo"]
Objects nest, which is how you model a real configuration shape rather than a pile of loose strings:
variable "network" {
type = object({
cidr = string
azs = list(string)
peering = object({
enabled = bool
remote_cidr = string
})
})
}
Tuples are fixed-length and position-typed — tuple([number, string, bool]) must always receive exactly three values in that order.
Topic 3: Why Declaring a Type Matters
Terraform coerces values, and the rules are not intuitive.
For a bool, all of these are valid: true, false, "true", "false", "1", "0". But bare 1 is not.
The reason: quoting makes Terraform evaluate the string’s content, so "1" is examined and converted. Unquoted 1 is simply a number and no conversion is attempted. For a number, a digits-only string coerces in the other direction.
This is the strongest argument for always declaring type. Without it, a variable takes whatever type it is handed, and a value arriving as a string from one source and a number from another produces behaviour that changes depending on how it was set — a genuinely miserable bug to track down.
Validation catches the rest:
variable "env" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "Environment must be one of dev, staging, prod."
}
}
A type constraint says what shape; validation says which values. Use both, and write an error message that tells the reader how to fix it.
Topic 4: Assignment Methods
export TF_VAR_environment=staging # environment variable
terraform apply -var="environment=prod" # CLI flag
terraform apply -var-file="prod.tfvars" # variable file
terraform apply -var='amis={"us-east-1":"ami-abc"}' # complex value
# prod.tfvars
environment = "prod"
instance_type = "t3.large"
desired_count = 3
Files named exactly terraform.tfvars (or .json) and anything ending .auto.tfvars load automatically with no flag.
Topic 5: Precedence
When the same variable is set more than once, later sources override earlier:
- Environment variables (
TF_VAR_*) terraform.tfvarsterraform.tfvars.json*.auto.tfvars/*.auto.tfvars.json, in lexical filename order-varand-var-fileon the command line, in the order given
Command-line flags win. Environment variables lose to everything else — which surprises people, because an exported variable feels like an override.
Verify this ordering against the documentation for your Terraform version rather than trusting any single write-up. Published material disagrees on where environment variables sit, and it is exactly the kind of detail that silently applies the wrong environment’s configuration.
Topic 6: Sensitive Variables
variable "db_password" {
type = string
sensitive = true
}
This redacts the value from CLI output and logs. Be precise about what that buys you: it suppresses display. The value is still written to state in plain text, and anyone who can read state can read it.
Keeping secrets out of state is a separate problem with a separate answer — covered in the security lesson.
Try it yourself: Declare a variable with no type, set it to "1" from a tfvars file, and use it where a bool is expected. Then declare it type = number and try again. The difference is the bug this lesson exists to prevent.
Common mistake: Using variables for values that never vary. A variable with one possible value is indirection with no benefit — it makes the reader chase a definition to learn a constant. Variables are for what changes between environments.