skip to content

Input Variables and Type Constraints

Typed variables catch mistakes at plan time instead of halfway through an apply. Object types with optional attributes are the modern way to give a module a rich but forgiving interface.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

5

In Terraform, what does the `type` argument in a `variable` block do, and what categories of type constraint can you declare?

level: juniorimportance: must knowfreq 72%

answer

  1. three families of constraint
  2. primitive, collection, structural
  3. conversion first, then rejection
  4. checked while variables are evaluated
  5. omitting type means any

basics

~20 s

Terraform's type argument constrains what a caller may pass, rejecting mismatches during plan before anything is created. Type constraints come in three families: primitives (string, number, bool), collections (list, set, map), and structural types (object, tuple).

solid answer

~50 s

The `type` argument on a `variable` block is a type constraint — a declaration of the shape of value the module accepts. There are three families. Primitives are `string`, `number` and `bool`. Collections — `list(T)`, `set(T)`, `map(T)` — hold any number of elements that must all share one element type. Structural types — `object({name = string, size = number})` and `tuple([string, number])` — describe a fixed shape where each attribute or position has its own type. There is also `any`, a wildcard, which is what you get if you omit `type` entirely. Terraform checks the supplied value against the constraint while evaluating variables at the start of a plan, converting where the conversion is unambiguous (the string `"5"` to the number `5`, a `["a","b"]` literal to `list(string)`) and failing the whole run where it is not. The payoff is that a bad input is caught in seconds rather than halfway through an apply.

code

hcl · 19 lines
hcl
variable "region" {
  type = string
}

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

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

variable "database" {
  type = object({
    engine   = string
    size_gb  = number
    multi_az = bool
  })
}

go deeper

for a junior

Be ready to name the three families and write a variable block with type, description and default. Say plainly that a wrong-shaped value fails the plan rather than the apply.

for a middle

Explain the conversion step: a bracketed literal is a tuple that Terraform converts to a list or set, and "5" converts to a number. Be able to nest a structural type inside a collection and justify it.

for a senior

Show why a strict interface is operationally cheap: a mis-shaped input caught during variable evaluation costs a terminal message, while the same mistake reaching apply leaves a partially built estate to unwind.

for a principal

Own the interface-stability angle: a module's type constraints are its contract with every consumer, so tightening one is a breaking change that belongs behind a version bump and a documented migration.

## What a type constraint actually is A `variable` block declares one input to a Terraform module — the root module included. Its `type` argument is not a runtime check bolted on the side; it is part of the module's published interface, and Terraform uses it to *convert* as well as to *reject*. ```hcl variable "instance_name" { type = string description = "Name tag applied to the instance" } ``` When Terraform evaluates the root module's variables at the beginning of a plan, it takes the value supplied for `instance_name`, attempts to convert it to `string`, and either succeeds (the module now sees a `string`) or aborts the entire run with an "Invalid value for input variable" error. For a child module, the same thing happens to each argument written in the calling `module` block. Nothing is created, nothing is refreshed against the API — the failure lands before the graph walk begins. ## The three families **Primitive types** are `string`, `number` and `bool`. Terraform has exactly one number type; there is no separate integer. **Collection types** — `list(T)`, `set(T)` and `map(T)` — hold zero or more elements that must all have the same type `T`. `list(string)` is ordered and indexed by position, `set(string)` is unordered and de-duplicated, `map(string)` is keyed by arbitrary strings. **Structural types** — `object({...})` and `tuple([...])` — describe a fixed shape whose members may each have a different type: ```hcl variable "database" { type = object({ engine = string size_gb = number multi_az = bool }) } ``` A caller passes `{ engine = "postgres", size_gb = 100, multi_az = true }`. Each attribute is checked against its own declared type. `tuple([string, number])` is the positional equivalent and is rare in hand-written code — it mostly shows up as the type Terraform infers for a mixed literal like `["a", 1]`. These nest freely: `list(object({name = string, port = number}))` is an ordinary and very useful constraint for something like a list of listener rules. ## Conversion comes before rejection Terraform is not strictly typed in the "exact match or die" sense. It applies automatic conversions where there is only one sensible reading: `"5"` converts to the number `5` for a `number` variable, `true` converts to `"true"` for a `string`, and a bracketed literal — which is really a *tuple* — converts to `list(string)` or `set(string)` when its elements can all become strings. A braced literal is an *object* and converts to `map(...)` on the same terms. Conversions that would lose information or are ambiguous, such as `"abc"` to `number`, fail. This is why a `.tfvars` file full of quoted values usually works against numeric variables: the conversion, not the author, did the work. ## Omitting the type If you leave `type` off, the variable's constraint is `any` and every value is accepted as-is. That is legal, and it is what a lot of older module code does, but it means the module has no declared interface: a caller learns the expected shape only by reading the module body or by watching the plan fail somewhere deep inside a resource argument. ## Why this matters more in infrastructure code than elsewhere Infrastructure code has an unusually expensive failure mode: a run that dies in the middle has already created some real resources and left the rest undone. Type constraints are the cheapest possible test in that world — they run in the first second of `terraform plan`, cost nothing, and need no credentials or test harness. Declaring `number` on a retention period and `object({...})` on a subnet definition converts a class of caller mistakes from a partially-applied estate into a message on the terminal. The practical rule for module authors: declare the tightest type that is honestly true. `string` beats `any`; `list(object({...}))` beats `list(any)`; and where the shape is genuinely rich but partly optional, the object type with optional attributes is the modern way to say so.

  • Where in the Terraform run does a type mismatch surface, and why does that timing matter?
    While Terraform evaluates variables at the start of the plan — for the root module from the supplied values, for a child module from the arguments in its `module` block. The run aborts before the graph is walked, so no resource has been created or changed. That is the difference between a terminal error and a half-applied estate you now have to reconcile.
  • What is the difference between a tuple and a list in Terraform, and when do you actually meet a tuple?
    A `list(T)` holds any number of elements that all share one type; a `tuple([...])` has a fixed length with a declared type per position. You rarely declare tuples, but you meet them constantly: every bracketed literal in HCL is a tuple, which Terraform then converts to the list or set the constraint asks for. A mixed literal like `["a", 1]` stays a tuple because no single element type fits.
  • Is a type constraint enough to guarantee a variable's value is usable?
    No. A constraint describes shape, not meaning: `number` accepts `-1` for a retention period, and `string` accepts `"eu-nowhere-1"` as a region. Types stop mis-shaped input; they say nothing about whether a well-shaped value is sensible. That is a separate concern from the type system.

saying these in an interview costs you the question

  • Thinking type constraints are checked only at apply time
  • Claiming Terraform has separate int and float types
  • Believing collections can hold mixed element types
  • Assuming omitting type makes the variable required
  • Treating object and map as interchangeable

context

open as a page

A Terraform variable can be typed `list(string)`, `set(string)` or `map(string)`. How do the three differ, and what does the module receive if a `set(string)` variable is given the value `["b", "a", "a"]`?

level: middleimportance: must knowfreq 62%

basics

~20 s

A list is ordered and allows duplicates, a set is unordered and de-duplicated, a map is keyed by strings. Given ["b", "a", "a"], a set(string) variable yields two elements — "a" and "b" — with the caller's ordering and the duplicate discarded.

open as a page

In Terraform 1.3 or later, how do you declare a module input that takes an object where some attributes may be omitted by the caller, and what value do the omitted attributes get?

level: middleimportance: should knowfreq 46%

basics

~10 s

Wrap the attribute's type in optional() inside the object type constraint. An omitted attribute declared optional(string) becomes null; optional(string, "t3.micro") supplies that default instead. Required attributes stay unwrapped and must be given.

open as a page

A Terraform module declares `variable "tags" { type = map(any) }` and a caller passes `{ Name = "web", Retention = 30 }`. What does the module actually receive, and why is `map(any)` a poor interface?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Terraform unifies a collection's element type to one concrete type, so the module receives a map of strings where Retention is "30". If no single element type fits — one value being a list, say — the whole plan fails instead. map(any) neither preserves mixed types nor checks anything.

open as a page

What does `nullable = false` do on a Terraform input variable, and how does it change what happens when a caller explicitly passes `null` to a variable that has a default?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

nullable = false forbids the value being null inside the module. With the default nullable = true, an explicit null overrides the default and the module sees null; with nullable = false, Terraform substitutes the default instead, or errors if there is none.

open as a page