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/terraform and choose slug tf-variable-validation; the lab runs real Terraform against the offline FakeCloud provider.