Terraform 1.1.4 repairs defaults and inferred resource moves
Two module-boundary bugs explain why variable defaults and automatic resource-address changes need consistent treatment inside Terraform.
A module can promise that an input will never be null and still have its validation expression receive null. Terraform 1.1.4, released on January 19, repairs that inconsistency. The same release fixes a resource-move error that callers could encounter after an external module added or removed count. Both changes concern how Terraform interprets a reusable module, as the release notes describe. Neither requires a new language construct.
I value Terraform for making an API-backed infrastructure definition reusable across configurations. That value depends on a module behaving according to its declared interface. A caller should not need to understand the order of Terraform's internal evaluation steps to pass an optional input correctly. These fixes make the abstraction more dependable in two specific cases.
Validation must receive the effective input
The input-variable documentation shipped with 1.1.4 distinguishes omission from an explicit null value. A variable with a default uses that default when the caller omits the argument. With the normal nullable = true setting, explicitly passing null overrides the default. Module authors therefore cannot assume that a default alone guarantees a non-null value.
Setting nullable = false changes that contract. When the variable also has a default, Terraform uses the default for an explicit null input. Inside the module, references should then observe the substituted value. This gives a caller a useful way to leave a decision to the module without reproducing its default in the calling configuration.
Consider the following child-module variable. The validation rule is deliberately simple so that the distinction between the supplied value and the effective value remains visible.
variable "input" {
type = string
default = "default"
nullable = false
validation {
condition = var.input != null
error_message = "Input cannot be null."
}
}
A parent module can pass input = null to this declaration. The expected validation input is the string "default". Rejecting the call because the original expression was null contradicts the declaration's own substitution rule. This is the case covered by the regression test in pull request 30330, which places a null argument in the parent and the non-nullable declaration in a child module.
The implementation history explains why a seemingly simple rule failed. Default handling occurred in two places, including graph transformation and evaluation of variable references. The validation path did not receive the substitution it needed. The patch applies a targeted correction when argument evaluation succeeds, the result is null, nullability is disabled, and a usable non-null default exists. Validation can then evaluate the value that the module contract actually promises.
That correction does not recursively prohibit null. The versioned documentation explicitly allows null elements or attributes inside collection and structural values even when the variable itself is non-nullable. An object input can therefore satisfy the top-level rule while containing a null attribute. Module authors still need type constraints and suitable validation expressions for the contents they intend to accept.
Inferred moves need different boundary checks
The other fix concerns an existing object's Terraform address. The versioned refactoring documentation explains that Terraform correlates configuration with state through addresses. A rename can otherwise appear to require destroying one object and creating another. A moved block records the relationship between the previous address and the current one.
Adding count changes a resource's addressing. The documentation says Terraform automatically proposes moving an existing single instance to index zero unless an explicit move for that resource says otherwise. The following declaration makes such a transition explicit. It belongs with the resource configuration in its owning module.
moved {
from = aws_instance.example
to = aws_instance.example[0]
}
Terraform also represents its automatic handling through implied move statements. Pull request 30333 explains that these identify a particular resource in a particular module instance through a path rooted at the root module. If an external module changes its resource's use of count, that generated path can cross a package boundary.
Ordinary move declarations have package restrictions because separately distributed modules should not depend on each other's internal resource organization. Applying the same restriction to an inferred move produced an incorrect cross-package diagnostic. The inferred source and destination belong to the same module instance, and Terraform derives them from information inside that module. The fix exempts this generated case from the inappropriate boundary rejection.
For the caller, the distinction matters because the resource change happened upstream. Editing the caller's arguments cannot undo the module author's addressing change. The patch restores Terraform's ability to handle that transition without requiring the caller to rewrite the external package.
I prefer explicit move declarations when maintaining reusable modules because they preserve the intended address mapping in the configuration. The documentation recommends that practice even for the automatic index-zero case. Keeping historical move declarations also preserves upgrade paths for callers whose state still contains older addresses. Terraform 1.1.4 corrects the inferred path while leaving that explicit migration history useful.