Input Validation - Terraform Tips & Tricks
Tags: beginners, terraform, devops, infrastructureascode
Published: 2024-03-10 00:20:11

Terraform [modules](https://developer.hashicorp.com/terraform/language/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. 

![Ralph Wiggum - Danger](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExdTJ6bXE1NjY4OWV5NjlzbzVqMzJmamxjdzVmMWhqN3Nuc2UxbHIwbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/55itGuoAJiZEEen9gg/giphy.gif)

In this post, I’ll be covering some of the methods I’ve used to enforce input standards.

### Table of Contents
{%- # TOC start -%}
- **[Structured Types](#structured-types)**
- **[Validation Blocks](#validation-block)**
   * [Going Further with Functions](#going-further-with-functions)
   * [Adding For Expressions](#adding-for-expressions)
- **[Wrapping Up](#wrapping-up)**
{%- # TOC end -%}

---

## 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.

```tf
variable "validation_environment" {
  type        = bool
  description = "Set as true to enable validation environment."
  default     = false
}
```

The input must be a number.

```tf
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.

```tf
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 object must have **4 key-value pairs** (**KPV**). Each key must match the defined name, "app_name" or "local_path". Each KPV value is also type defined: string, number, bool, etc.

```tf
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.

```tf
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. 

```tf
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. 

```tf
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**](https://developer.hashicorp.com/terraform/language/functions). 

In the example below, the [**anytrue**](https://developer.hashicorp.com/terraform/language/functions) function evaluates the input against each statement. If any of the three statements within evaluate to **true**, Terraform will proceed.

```tf
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**](https://developer.hashicorp.com/terraform/language/functions) 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](https://developer.hashicorp.com/terraform/language/expressions/for) can be used alongside functions to build more thorough validation rules. 

1. Anytrue evaluate every string as it loops each of the ingested list's values.

2. The result of each loop is compiled into a new list of bools.

3. Finally, the list of bools is evaluated against the [**alltrue**](https://developer.hashicorp.com/terraform/language/functions) function. If any value in the list is **false**, the condition will fail and trigger error_message.

```tf
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. 

![Ralph Wiggum - Goodbye](https://www.reactiongifs.com/wp-content/uploads/2013/07/ralph-wave.gif)