Variable Types and Validation
Terraform has a real type system — primitives (string, number, bool), collections (list, set, map), and structural types (object, tuple) — plus validation blocks that reject bad input before any apply runs. Strong types and validation turn a module into a contract: callers get an immediate, clear error instead of a confusing failure 40 resources deep into an apply.
The payoff is moving failures left. A string typed as a string accepts anything; an object with validation accepts exactly the shape you meant. The difference between those two is whether a caller's mistake surfaces at plan time with a sentence telling them what's wrong, or at apply time as a cryptic provider error after half the stack already changed.
Primitive and Collection Types
The primitives are string, number, and bool. The collections wrap a single element type: list(string) is ordered and allows duplicates, set(string) is unordered and deduplicated, and map(string) is key-value with named access. The choice is not cosmetic — it changes how the value behaves in a plan and how you access it.
variable "desired_count" { type = number } variable "availability_zones" { type = list(string) } variable "resource_tags" { type = map(string) }
Structural Types
An object({...}) describes a value with named attributes of mixed types, and optional() marks an attribute the caller may omit, supplying a default when they do. This is how a module accepts one richly-shaped input instead of a dozen loose variables. The block below takes a single app_config object where instance_type defaults to t3.micro if the caller leaves it out.
variable "app_config" { type = object({ name = string instance_type = optional(string, "t3.micro") min_size = number public = optional(bool, false) }) }
Validation Blocks
A validation block runs a condition against the input and, if it fails, halts the plan with your error_message. Use it for ranges, allowed values, and formats — anything where a wrong value is knowable before apply. The validation below rejects an environment that isn't one of three allowed strings, and the message tells the caller exactly what's valid.
variable "environment" { type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be one of: dev, staging, prod." } }
Type Conversion and Nullability
Terraform converts between compatible types automatically — a number flows into a string context, a list converts to a set — and the surprises come when an implicit conversion isn't the one you expected, like a number-as-string comparison that never matches. By default a variable may be null; setting nullable = false forbids it, and optional() with a default sidesteps the null question entirely for object attributes.
list — ordered and allows duplicates, accessed by numeric index. Choose it when order is meaningful (a sequence of CIDR blocks assigned to subnets in order) and you need positional access.
set — unordered and deduplicated, with no stable index. Choose it for a collection of distinct values where order is irrelevant — it avoids the noisy diffs a reordered list produces, and it is the natural source for for_each.
map — key-value pairs accessed by name. Choose it when each element needs a stable identifier (per-environment settings keyed by environment name) rather than a position.
- Typing a structured input as
stringand parsing it yourself, pushing what should be a plan-time type error into a runtime failure deep in the apply. - Using a
listwhere order carries no meaning, getting a destroy/recreate churn every time the order shifts; asetdeduplicates and ignores order. - Writing an
error_messagethat says "invalid value" without naming the constraint, so the caller still has to read the source to learn what's allowed. - Relying on implicit conversion and being bitten when a number compared as a string never equals the number you meant.
- Leaving a variable
nullablewhen a downstream resource can't accept null, turning a clear input error into a confusing provider rejection.
- Type variables as precisely as the input deserves; use
objectwithoptional()for structured configuration instead of a scatter of loose variables. - Add
validationblocks for ranges, allowed values, and formats so callers fail fast at plan time with a clear message. - Write each
error_messageto state the constraint and give a valid example, so the caller fixes it without reading the code. - Choose
list,set, ormapby access pattern and diff behavior, not by habit — asetfor unordered distinct values avoids reorder churn. - Set
nullable = falseon any variable a downstream resource can't accept null for, catching the omission at the boundary.
Knowledge Check
You have a collection of distinct subnet IDs where order does not matter and you want to feed it to for_each. Which type fits best?
- A
set, because it is unordered and deduplicated, avoiding reorder churn - A
list, because its positional numeric indexes give each ID the most stable identity - A
stringof comma-separated IDs that you split back apart at the point of use - A
tuple, because it enforces a fixed length and per-position types
What does a validation block buy you over leaving the variable untyped?
- A bad value is rejected at plan time with your error message, before any resource changes
- It encrypts the variable's value at rest in the state file automatically
- It makes the variable required even when you have not yet removed its declared default value
- It retries the apply automatically whenever the supplied value is wrong
What does optional(string, "t3.micro") inside an object type do?
- Lets the caller omit that attribute, substituting the default when they do
- Forces the attribute to always be set to that one exact constant value
- Marks the entire object variable itself as optional for the caller
- Validates that the attribute's value matches the given regular expression pattern
Why does typing an input precisely as an object reduce runtime failures?
- A shape mismatch is caught as a plan-time type error rather than surfacing as a provider failure mid-apply
- Object-typed values are applied faster than loose strings because Terraform caches their parsed shape across runs
- Object-typed variables are automatically marked sensitive and redacted from output
- Terraform skips the state refresh step entirely for object-typed inputs
You got correct