Variables, Types & Precedence

All eight variable types including the structural ones, why declaring a type prevents a class of silent coercion bugs, validation blocks, and the assignment precedence order that decides which value actually wins.

intermediate 20 min lesson hands-on task included

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

TypeBehaviour
string, number, boolPrimitives
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
anyPlaceholder; 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:

  1. Environment variables (TF_VAR_*)
  2. terraform.tfvars
  3. terraform.tfvars.json
  4. *.auto.tfvars / *.auto.tfvars.json, in lexical filename order
  5. -var and -var-file on 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.