Terraform has a small, strict type system. Use it so that mistakes show up as a clear error at plan, not as a half-built environment.
flowchart LR
V["variable apps (typed)"] --> VAL["validation"]
VAL --> LO["locals + for expressions"]
LO --> PRE["precondition"]
PRE --> RES["resource"]
RES --> POST["postcondition"]
%% step V: A type constraint such as map(object({...})) rejects input of the wrong shape.
%% step VAL: validation blocks check the values themselves and print your error message.
%% step LO: locals and for expressions reshape input into exactly what resources need.
%% step PRE: A precondition checks assumptions before Terraform creates or changes the resource.
%% step POST: A postcondition checks the result after the resource or data source is read.
Types and optional attributes
variable "apps" {
type = map(object({
image = string
port = number
replicas = optional(number, 1) # default when the caller omits it
env = optional(map(string), {})
}))
}
Primitive types: string, number, bool. Collections: list(...), set(...), map(...). Structural: object({...}), tuple([...]). Mark secrets with sensitive = true (module 5) and use nullable = false when null must not be passed.
validation
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging or prod."
}
}
Since Terraform 1.9 a validation condition may also refer to other variables.
locals and for expressions
locals {
name_prefix = "shop-${var.environment}"
public_apps = { for k, a in var.apps : k => a if a.port < 9000 }
all_ports = [for a in values(var.apps) : a.port]
tags = merge(var.common_tags, { env = var.environment })
}
[for ...] builds a list, {for k, v in ... : k => v} builds a map, and if filters. Other everyday tools: conditional cond ? a : b, splat docker_container.app[*].name, try(), coalesce(), lookup(), flatten().
Pre- and postconditions
resource "docker_container" "web" {
# ...
lifecycle {
precondition {
condition = var.environment != "prod" || var.replicas >= 2
error_message = "prod needs at least 2 replicas."
}
postcondition {
condition = self.exit_code == null
error_message = "the container exited right after start."
}
}
}
| Check | Runs | Blocks |
|---|---|---|
| Type constraint | Before anything | Wrong shape of input |
validation |
On the variable | Bad input value |
precondition |
Before create/update | Broken assumption |
postcondition |
After create/read | Unexpected result |
check block (module 3) |
Every plan/apply | Nothing: it only warns |
Traps: map(any) hides typos that object() would catch; and a failing precondition stops the run, while a failing check only prints a warning.