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.
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
1. What happens if a variable has no default value and no value is supplied when running Terraform non-interactively (e.g. in CI)?
2. Which of these correctly describes the effect of setting sensitive = true on a variable?
3. Which variable value source takes the highest precedence in Terraform?
4. What is the purpose of a validation block inside a variable declaration?
5. In an object type constraint, what does wrapping an attribute in optional(type, default) achieve?
Was this page helpful?
You May Also Like
HCL Syntax Fundamentals
HashiCorp Configuration Language (HCL) is the declarative syntax underlying every Terraform file. Mastering its blocks, arguments, and expressions is the foundation for writing correct configurations.
Outputs and Locals
Output values expose data from a module for use by callers or the CLI, while local values let you name and reuse computed expressions within a module. Together they make configurations readable and composable.
Writing a Reusable Module
Turning ad-hoc configuration into a well-designed reusable module requires thoughtful input/output design, sensible defaults, and internal structure. This topic walks through the practical craft of module authoring.
Expressions and Functions
HCL expressions let you reference values, perform operations, and transform data using Terraform's extensive built-in function library. This topic covers references, operators, conditionals, and common functions.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics