Primitive types
There are three base types:
| Type | Literal | Example |
|---|---|---|
string | in double quotes | "us-east-1" |
number | integer or decimal | 42, 3.14 |
bool | true / false | true |
In a variable you declare the type explicitly:
variable "region" {type = string
default = "us-east-1"
}
variable "retention_days" {type = number
default = 30
}
variable "enable_versioning" {type = bool
default = false
}
Complex collection types
A collection is "many of the same." Every element has the same type.
list (ordered)
variable "az_names" {type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
You access elements by index: var.az_names[0] gives "us-east-1a". Order
matters and stays stable.
set (no order, no duplicates)
variable "allowed_cidrs" {type = set(string)
default = ["10.0.0.0/8", "192.168.0.0/16"]
}
There is no order, so you cannot access elements by index. A set is used with
for_each, which needs unique keys with no order.
map (key-value, string keys)
variable "tags" {type = map(string)
default = {Environment = "dev"
Owner = "platform"
}
}
You access values by key: var.tags["Environment"] gives "dev". All values
have the same type (string in this example).
Structural types
A structural type is "many different values of a shape known in advance."
object (fields of different types)
variable "bucket_settings" { type = object({name = string
versioning = bool
retention_days = number
})
default = {name = "my-bucket"
versioning = true
retention_days = 90
}
}
Fields can have different types. You access them with a dot:
var.bucket_settings.name. If the default leaves out a field or adds an
extra one, terraform fails on validate.
tuple (an ordered pair or triple)
variable "name_and_size" {type = tuple([string, number])
default = ["primary", 100]
}
A tuple is like a list, but each element has its own type and the length is fixed. In practice you rarely need one: an object is usually clearer.
How list differs from tuple
list(T)holds any number of elements of one typeT.tuple([T1, T2, T3])holds exactly three elements, each of its own type.
If your colleagues keep writing the same shape, that is a list. If the structure has several fields, an object is preferable to a tuple.
How map differs from object
map(T)has an arbitrary number of keys, all values of typeT.object({...})has a fixed set of keys, each value of its own type.
A tag set is a map(string). A resource config is an object({...}).
A simple rule of thumb: if the keys come from outside (from the user, not
from your code), use map; if you write the key names yourself, use
object.
any, opting out of typing
variable "anything" {type = any
}
Terraform will not check the structure at all. This is an escape hatch for
cases where the type changes at runtime, and it is almost always an
anti-pattern. If you use any, write a comment explaining why typing was
genuinely impossible.
Optional fields in an object
Since Terraform 1.3+ you can make object fields optional:
variable "logging" { type = object({enabled = bool
target_arn = optional(string)
prefix = optional(string, "logs/") # default
})
default = {enabled = true
}
}
optional(string)means the field can be omitted; its value will benull.optional(string, "logs/")sets a default value.
This saves you from long object schemas where half the fields are rarely needed.
Gotchas
nullis not the same as a missing field. In an object with required fields you must pass them all, evennullexplicitly.map(string)versusobject({a=string})in a variable. Withmap(string)the keys can be anything; withobject, only the ones you listed.- A set loses order. If order matters (for example, in outputs), switch to a list.
- Numbers in HCL have no fixed precision. terraform stores a number
internally as
cty.Number(BigFloat). Large values may round, but this is usually not a problem. - A boolean from a string.
"true"(a string) andtrue(a bool) are different types. terraform does not convert automatically, so you get a type error.