100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
HCL

Variables and Variable Types

Input variables let Terraform configurations be parameterized and reused across environments. This topic covers declaring variables, type constraints, defaults, validation, and how to supply values.

HCL Language BasicsBeginner9 min readJul 9, 2026
Analogies

Variables and Variable Types

Input variables are how you parameterize a Terraform configuration so the same code can be reused across environments, accounts, or teams without editing the source. A variable block declares a named input with an optional type constraint, default value, description, and validation rules. Elsewhere in the configuration you reference its value with var.<name>. Variables are the primary interface for a root module or a reusable child module — they define what a caller is expected (or allowed) to supply.

🏏

Cricket analogy: A franchise's team sheet lets a squad be filled in per match without rewriting the whole roster; each slot has a defined role like opener or wicketkeeper, and the batting order is the interface a selector fills in before every game.

Declaring a Variable

The type argument constrains what values are acceptable. Terraform supports primitive types (string, number, bool), collection types (list(<TYPE>), set(<TYPE>), map(<TYPE>)), and structural types (object({...}), tuple([...])). If you omit type, Terraform infers any, which accepts anything but forfeits the type-checking safety net — omitting it is discouraged outside of quick prototypes. A default makes the variable optional; without one, Terraform will prompt interactively or fail in non-interactive contexts like CI if no value is supplied.

🏏

Cricket analogy: A team sheet accepts only specific entries: a player's name (a string), their shirt number (a number), or whether they're playing (yes or no); leaving a role blank lets anyone claim it, risky outside a friendly practice match. If no role is specified, any player could theoretically be slotted anywhere, which is fine for a casual net session but risky for a real match, so selectors always specify roles explicitly. A default batting position makes selection optional; without one, the selectors must decide before the toss or the team sheet is incomplete.

hcl
variable "environment" {
  type        = string
  description = "Deployment environment name"
  default     = "staging"

  validation {
    condition     = contains(["staging", "production"], var.environment)
    error_message = "environment must be either 'staging' or 'production'."
  }
}

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

variable "subnet_ids" {
  type = list(string)
}

variable "tags" {
  type    = map(string)
  default = {}
}

variable "db_config" {
  type = object({
    engine  = string
    version = string
    storage = optional(number, 20)
  })
}

Supplying Values

Terraform resolves variable values from multiple sources, in ascending order of precedence: environment variables prefixed TF_VAR_<name>, a terraform.tfvars file, any *.auto.tfvars files (loaded automatically in alphabetical order), -var-file command-line flags (in the order given), and finally -var flags on the command line, which win over everything else. Understanding this precedence matters when debugging why a value isn't what you expect — a stray .auto.tfvars file is a frequent culprit.

🏏

Cricket analogy: When picking a team, the captain's own preference (called last) overrides the coach's suggestion, which overrides the standard batting-order template, which overrides the historical default — knowing this order matters when a surprising selection appears.

Marking a variable sensitive = true redacts it from CLI plan/apply output, but it is NOT encryption — the value is still written to the state file in plaintext (unless your backend encrypts state at rest) and can still be exposed via terraform state show or terraform output if not also marked sensitive there.

validation blocks let a module author fail fast with a clear error message instead of letting an invalid value propagate into a confusing provider API error deep inside apply. Well-written validation messages are one of the biggest usability differences between a good internal module and a frustrating one.

Terraform's type system performs some automatic conversion — a string "3" can often satisfy a number constraint — but explicit types still matter because they catch structural mistakes (like passing a single string where a list(string) is required) at plan time rather than deep inside a resource's apply logic. The optional() modifier inside object type constraints, as shown above, lets specific object attributes be omitted by the caller and fall back to a default.

🏏

Cricket analogy: A scorer's tally sheet can sometimes auto-convert a written number like 'four' into the numeral 4, but a mismatched entry, like writing a single player's name where a full batting order list is expected, is caught before the match starts, not mid-innings. A special note can let one optional field, like a nightwatchman's role, be skipped entirely and default to the standard batting order otherwise.

  • Variable blocks parameterize configuration; reference values with var.<name>.
  • Type constraints include primitives, collections (list/set/map), and structural types (object/tuple).
  • Precedence for supplying values: -var flags > -var-file > *.auto.tfvars > terraform.tfvars > TF_VAR_ environment variables (roughly, CLI wins over files wins over env).
  • sensitive = true hides a value from CLI output but does not encrypt it in state.
  • validation blocks enforce constraints beyond type, with a custom error_message.
  • optional() inside object types lets specific attributes have defaults when the caller omits them.

Practice what you learned

Was this page helpful?

Topics covered

#HCL#TerraformInfrastructureAsCodeStudyNotes#DevOps#VariablesAndVariableTypes#Variables#Variable#Types#Declaring#StudyNotes#SkillVeris