Input Variables as Contracts
Scenario: A module accepts any, then fails deep inside a resource because a caller supplied a list where a structured object was expected.
Variables define a module's public input contract. Use precise type constraints, meaningful descriptions, safe defaults only when a universal default exists, nullable deliberately, and validation for domain rules. Object types group related values and make evolution clearer than parallel variables. Validation errors should tell callers exactly how to recover.
Analogy: A variable block is an API schema. Strong types reject malformed requests at the door instead of letting them fail inside a distant provider call.
A worked configuration
variable "service" {
type = object({
name = string
port = number
tier = optional(string, "standard")
})
validation {
condition = var.service.port >= 1 && var.service.port <= 65535
error_message = "service.port must be a valid TCP port."
}
}
Production modules should distinguish operator choices from derived facts. Every exposed variable becomes a compatibility promise; exposing every provider argument creates a wrapper with no opinion and a huge support surface.
Note: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it.
Goal: Reinforce this lesson in the tf-variable-validation Terraform lab. Open/labs/terraformand choose slugtf-variable-validation; the lab runs real Terraform against the offline FakeCloud provider.