Design by Contract in Terraform
Understanding custom condition checks
In this article, I present a perspective on custom condition checks as a design by contracts approach for writing Terraform modules.
In 2020, the Terraform team introduced custom validation rules for input variables as an experimental feature and later released it as a stable feature in v0.13. This year, they extended custom validations by introducing preconditions and postconditions in the v1.2.0 release, which are for use in resource, data source, and output blocks.
Both the above introduce a way that enables Terraform authors to define a precise contract for modules that specifies obligations and guarantees for a module caller.
In software design, this is called Design by contract, a term coined by Bertrand Meyer¹. Its central idea is a metaphor for how elements of a software system collaborate with each other on the basis of mutual obligations and benefits².
A software component provides a contract, for the services that it will provide, and that by using the component a client agrees to the terms of that contract³.

The contract is a list of specifications. The specifications may include:
- Preconditions: the client is obligated to meet a function’s required preconditions before calling a function. If the preconditions are not met, then the function may not operate correctly.
- Postconditions: the function guarantees that certain conditions will be met after it has finished its work. If the postcondition is not met, then the function did not complete its work correctly
- Acceptable and unacceptable input values
The mechanisms for expressing obligations and guarantees are assertions. Having assertions as part of the code makes contracts a useful tool to check for correctness. Consider the following function for computing a square root:
square_root(x):
assert x > 0 # precondition
...
/*a computation to get the square root of x as y*/
...
assert y*y == x # postcondition
return y
Contracts in Terraform
In terms of Terraform, the assertions are represented by a condition
argument inside a validation
/ precondition
/ postcondition
block. The condition
takes an expression which must evaluate to true
/ false
. If the assertion is violated, Terraform will return an error_message
and halt the execution.
condition = self.private_dns != ""
error_message = "The instance must be in a VPC that has private DNS hostnames enabled."

Where Custom Condition Checks appear in various block types

Custom Condition Checks Capture Assumptions and Guarantees

Notice the above uses the word “assumption”, instead of “obligation”. This may be a little confusing, so to clear it up consider the following points of view:
The resource:
The resource must ensure the postconditon is met: makes a gaurantee for it’s caller. The resource may make an assumption about how the caller is using it : defines a preconditions that must be met by the caller.
The caller:
The caller must ensure the precondition defined by the resource is met and may assume some guarantees made by the resource (postcondition).

A comparison of how Terraform Custom Condition Checks can be used

References
[1] Bertrand Meyer, in Applying “Design by Contract” (October 1992). IEEE Computer.
[2] Wikipedia Contributors, Design by contract (June 2022). Wikipedia.
[3] Martin Reddy, in API Design for C++ (2011). 9.1.2 Documenting the Interface’s Contract.
Want to connect
If you think this was helpful and would like to show your support here's my :
PayPal page
Buy me a coffee page
Ko-fi page