June 26, 2024·4 min read·engineeringterraformvalidationmodulestemplates

Terraform 1.9 lets validation express relationships between inputs

Terraform 1.9 removes the single-variable limit on input validation. Its templatestring function separately allows templates fetched as data to participate in configuration evaluation.

A module can accept two individually valid numbers whose combination is invalid. A lower capacity limit can exceed an upper limit even when both values satisfy their own type constraints. Before Terraform 1.9, an input variable's validation expression could refer only to that variable. The module author had to express a relationship elsewhere or package related settings into one input value.

The Terraform 1.9.0 release, dated 26 June, removes that reference restriction. Validation expressions can now refer to other variables and other objects in the same module, including data resources. This makes the module's input declarations capable of describing relationships that callers already need to satisfy.

I value Terraform's module model because it turns a collection of API operations into a reusable interface. An interface is more useful when it can state which combinations of inputs are meaningful. This release lets that requirement remain beside the input it constrains, without forcing unrelated representation choices onto callers.

A condition can describe the pair

The custom conditions documentation at the 1.9.0 tag records the old restriction and the new availability. A validation block still needs a condition that returns a boolean and an error message for the invalid case. Referring to another input expands the expression's available context.

For example, two numeric inputs can express a lower and upper bound directly.

variable "maximum_capacity" {
  type = number
}

variable "minimum_capacity" {
  type = number

  validation {
    condition     = var.minimum_capacity <= var.maximum_capacity
    error_message = "minimum_capacity must not exceed maximum_capacity."
  }
}

The example isolates the cross-variable relationship. It does not attempt to define every rule a capacity API might impose, such as whether numbers must be integers or nonnegative. Those would be additional conditions. The language change is that the second declaration can now compare the two separate inputs.

This matters for existing module interfaces. Combining settings into an object can be a sensible design when they represent one concept, but grouping them solely to make validation possible couples representation to a language limitation. A module with established separate variables can now add the relationship without requiring callers to rewrite their argument structure.

The error message also has a specific audience. It tells the caller which input relationship failed in the terms exposed by the module. A failure deep in a resource argument or remote service request can be harder to connect to those public inputs. Keeping the relationship in validation gives the configuration a place to document that connection.

Validation becomes part of the dependency structure

The release notes describe validation expressions as general expressions comparable to those elsewhere in a module. References to data resources are therefore possible as well as references to other inputs. A rule can compare an input with information obtained elsewhere, instead of relying only on the input's local syntax.

That broader context changes what an author should assume about timing. A reference to another caller-supplied number is a different dependency from a reference to information that has to be obtained from a remote system. The rule's location in a variable declaration does not make every referenced value immediately available. Configuration authors still need to understand the objects on which their conditions depend.

Terraform's custom-condition documentation distinguishes the roles of its condition mechanisms. Input validation checks requirements on inputs. Preconditions and postconditions describe requirements around resources and outputs. Check blocks assess infrastructure without blocking the overall operation. The new references do not make those mechanisms interchangeable.

For example, a relationship between two capacity inputs fits the public input contract. A guarantee about a value returned by a newly created resource has a different role. Choosing the condition location remains a design decision even when the language permits broader references. Terraform 1.9 removes an expression restriction, not the need to identify what a condition promises.

The documentation also requires the condition expression itself to avoid errors. A predicate intended to reject an input must actually evaluate to false for the invalid case. When an operation such as a regular expression match can raise an error, the documented can function can turn that failure into a boolean decision. Broader references do not remove those expression-level requirements.

Templates can arrive as data

The other headline language addition is templatestring. The versioned function documentation describes its primary use as rendering a template obtained as a string from a remote location. It complements templatefile, which reads template content from a file.

The new function takes a reference to a named object and an object containing template variables. Its documentation illustrates a reference to the body returned by an S3 object data source. The template can therefore enter configuration through a data resource, while the rendering variables remain explicit arguments at the call site.

This is different from writing a normal template expression directly in configuration. A direct template is already source code in the module. A retrieved string arrives as data, and templatestring explicitly asks Terraform to interpret it as template content. The additional interpretation step is why provenance of that string matters.

The function's first argument must reference an object defined in the module. The documentation permits constructing a template dynamically, assigning it to a local value and then referring to that local. It also warns that such construction can be difficult to maintain and can expose unexpected behavior through inputs. The restriction favors a visible named source for the content being interpreted.

Most consequentially, the documentation notes that Terraform template expressions can call built-in functions that interact with the local filesystem. A template obtained from elsewhere can therefore affect which local data enters the rendered result. Storing a template remotely does not make it a passive text file once Terraform interprets its contents.

I find both additions useful when their contracts remain explicit. Cross-variable validation states relationships at a module's input boundary. Dynamic templates name the source of text that will be interpreted and the values supplied to it. Terraform 1.9 expands what ordinary expressions can depend on, which makes those dependencies part of understanding the configuration.