Terraform modules are a great way to follow D.R.Y. development principles. I’ve written about D.R.Y. in the past, but to sum it up briefly:
The DRY (“Don’t Repeat Yourself”) principle follows the idea of every logic duplication being eliminated by abstraction. This means that during the development process we should avoid writing repetitive duplicated code as much as possible.
Writing a module for use by others on your team or the community at large can present some challenges.
If someone isn’t familiar with my code – they may input a value that I did not account for.
Incorrect input can cause deployment failure or, even more frightening, deploy an incorrect configuration without being immediately apparent.
In this post, I’ll be covering some of the methods I’ve used to enforce input standards.
Table of Contents
Structured Types
You’ll see the most basic form of type enforcement everywhere you look.
The variable below enforces the input standards with the type argument.
This variable will only accept a bool, true or false.
variable "validation_environment" {
type = bool
description = "Set as true to enable validation environment."
default = false
}
The input must be a number.
variable "maximum_sessions_allowed" {
type = number
description = "The maximum number of concurrent sessions per host."
default = 3
}
You can nest and combine these types using basic Terraform syntax. In the example below, Terraform must receive a list. Even if the list only contains a single string, it is a list of one string.
variable "included_location_ids" {
type = list(string)
description = "A list of Named Location IDs that will this policy will apply against."
default = ["All"]
}
In the next example, the type is defined as a map of objects. We’ve added a few other conditions as well – Each objects must have 4 key-value pairs (KPV). Each key must match the defined name, “app_name” or “local_path”. Each value of the key-value pair is also type defined: string, number, bool, etc.
variable "application_map" {
type = map(object({
app_name = string
local_path = string
aad_group = string
cmd_argument = string
}))
description = "A map of all applications and their metadata."
default = null
}
Lets reconsider enforcing the cmd_argument KPV from the object.
variable "application_map" {
type = map(object({
app_name = string
local_path = string
aad_group = string
}))
description = "A map of all applications and their metadata."
default = null
}
This change did remove the enforcement of cmd_argument, but it also barred its presence completely.
variable "application_map" {
type = map(object({
app_name = string
local_path = string
aad_group = string
cmd_argument = optional(string)
}))
description = "A map of all applications and their metadata."
default = null
}
The optional modifier allows cmd_argument to be included within the object, but it won’t reject an object without it.
Validation Block
The condition argument succeeds if it evaluates to true. If the statement evaluates to false, Terraform cancels the operation and outputs the string value of the error_message argument.
variable "stars" {
type = number
description = "Please rate your sanctification with my module. Select the number of stars, 1 through 5."
default = 5
validation {
condition = (
var.stars >= 1 &&
var.stars <= 5
)
error_message = "Please select a number of stars 1 through 5."
}
}
Going Further with Functions
We can expand the validation block use with Terraform functions.
In the example below, the anytrue function evaluates the input against each statement. If any of the three statements within evaluate to true, Terraform will proceed.
variable "primary_color" {
type = string
description = "The primary color of your choice!"
validation {
condition = anytrue([
lower(var.primary_color) == "blue",
lower(var.primary_color) == "red",
lower(var.primary_color) == "yellow"
])
error_message = "The var.primary_color input is incorrect. Please select blue, red, or yellow."
}
}
Did you notice the lower function was used here as well? We don’t care if the user inputs “BLUE” or “blue” do we? They’re both valid primary colors.
Adding For Expressions
For Expressions can be used alongside functions to build more thorough validation rules.
-
Anytrue evaluate every string as it loops each of the ingested list’s values.
-
The result of each loop is compiled into a new list of bools.
-
Finally, the list of bools is evaluated against the alltrue function. If any value in the list is false, the condition will fail and trigger error_message.
variable "excluded_platforms" {
type = list(string)
description = "The policy will enforce if the sign-in comes from the listed device platform(s)."
default = ["none"]
validation {
condition = alltrue([
for i in var.excluded_platforms : anytrue([
i == "none",
i == "all",
i == "android",
i == "iOS",
i == "linux",
i == "macOS",
i == "windows",
i == "windowsPhone",
i == "unknownFutureValue"
])
])
error_message = "Invalid input for included_platforms. The list may only contain the following value(s): none, all, android, iOS, linux, macOS, windows, windowsPhone or unknownFutureValue."
}
}
Wrapping Up
Validation blocks and structured types are both excellent tools for input validation and consistency.
Mix and match both techniques for the best outcome.