The Ultimate Guide to Terraform Input Variable Validation

Table of Contents

    Helpful HashiCorp Links For Module/Variable Validation

    Overview

    Terraform input variable validation is a Terraform feature that enables you to enforce data quality or data type constraints on the input variables in your Terraform configuration. This is especially useful when writing modules, as it allows you to enforce the correct usage of the module and provide clear error messages when an input is formatted incorrectly.

    Provider-Level Validation

    It’s important to note that many Terraform providers perform their own validation on input variables.  For GCP, for example, this may be a validation on a project name length or format prior to creation of that resource. This is generated by the API itself and is typically provided at the apply step of the Terraform workflow.

    One advantage of provider-level validations is that they are kept up-to-date relative to provider version updates.

    An example of provider-level input validation would be the configuration and error message below:

    resource "google_sql_database_instance" "postgres" {
        name                = "pg_instance"
        database_version    = "POSTGRES_13"
        region              = "asia-northeast1"
        deletion_protection = false
    
        settings {
            tier      = "db-f1-micro"
            disk_size = 10
        }
    }
    
    resource "google_sql_user" "users" {
        name     = "postgres"
        instance = google_sql_database_instance.postgres.name
        password = "admin"
    }
    $ Error: Error, failed to create instance pg_instance: googleapi: Error 400: Invalid request: instance name (pg_instance)., invalid

    The Downsides of Relying on Provider-Level Validation Only

    Validation is evaluated at the apply step, meaning user feedback is delayed.

    SRE principles emphasize quick feedback and rapid iteration to maintain high availability and reliability of services. delayed feedback can potentially lead to longer resolution times in case of misconfigurations, thus potentially affecting the service’s error budget and increasing toil for the operators, as they would have to spend additional time rectifying the configurations and re-applying them.

    Error messages are frequently enigmatic, broad, and lacking specifics on what is erroring your run.

    When error messages are not clear or specific, it requires additional effort from users to diagnose and rectify issues (operational overhead). It can also lead to longer Mean Time To Recovery (MTTR) in incident management, reducing the overall reliability of Terraform runs. User satisfaction and trust are negatively impacted as a result. I’ve seen it before.

    Clear, actionable, and specific error messages are crucial for rapid troubleshooting in order to minimize downtime and maintain high service quality.

    The Benefits of Terraform-Level Variable Input Validation

    Terraform input variable validation differs from provider-level validation in that it Terraform input variable validation is evaluated at the plan step of the Terraform workflow whereas provider-level validation occurs during the apply step. 

    There are a few distinct benefits of input validation:

    Better than API Information

    • Sometimes, error messages from the API can be difficult to understand or lacking in detail. By validating Terraform variables, you can ensure that the error messages displayed are clear and descriptive, which can make it easier for users to resolve issues with their inputs.

    Early Error Detection

    • Because input validation is evaluated at the plan step instead of the Terraform apply step, a user is prevented from progressing on to the apply stage with erroneous configurations.

    Enhanced Security

    • By thoroughly validating inputs, developers can mitigate potential security vulnerabilities stemming from insecure variable input.

    A Better User Experience Overall

    • Immediate Feedback: Offers quick error identification at the planning stage.
    • Helpful User-Friendly Error Messages: Provides specific and clear error details, reducing user frustration.
    • Consistency: Ensures a uniform and intuitive user experience across the platform.
    • Streamlined Development Process: Enables a more efficient and optimized development workflow.
    • User Confidence: Builds user trust by validating inputs and preventing inadvertent mistakes.
    • Educational Value: Informs and educates users about system constraints and requirements.

    Enhanced Error Clarity

    • Granular Error Identification: Helps in pinpointing and correcting input errors swiftly, improving the reliability of Terraform configs.
    • Environment-Specific Clarity: Provides error messages that may be tailored or specific to an organization’s unique environment and restrictions, including org-level policies, Sentinel policies, or Wiz.io policies.

    Feedback in the Terraform Workflow

    The mermaid.js chart below illustrates the Terraform workflow and where input variable validation provides user feedback within that workflow.

     sequenceDiagram participant User participant Terraform Plan participant Terraform Apply participant Provider User->>Terraform Plan: terraform plan (With Invalid Input) alt With Input Validation Terraform Plan->>User: Error: Invalid input else Without Input Validation Terraform Plan->>User: Plan: 1 to add, 0 to change, 0 to destroy. end User->>Terraform Apply: terraform apply (Approves the Plan) alt Without Input Validation Terraform Apply->>Provider: Create Resource (With Invalid Input) Provider->>Terraform Apply: Error: Invalid Input Terraform Apply->>User: Error applying plan: Invalid Input from Provider else With Input Validation Note right of User: User doesn’t reach this stage <br/> due to error in Plan stage. end

    Variable Validation Development Best Practices

    Terraform has a few requirements for variable validations ; it requires that error messages start with a capital letter and end with a period or question mark. Besides those current limitations, there are some additional best practices when writing variable validation.

    Granular Validation Blocks:

    • When building validations, make the individual validation blocks as granular as possible to ensure specific error messages are returned to users when an error occurs.
    • By making validation blocks more granular, we’re able to provide users more precise and specific error messages, significantly aiding in diagnosing and resolving configuration issues.
    • The validation below combines multiple validations in a single block:
    variable "name" {
      type = string
    
      validation {
        condition     = length(var.name) <= 64 && can(regex("^[^_]*$", var.name)) && can(regex("^[a-z0-9]*$", var.name))
        error_message = "The name must not exceed 64 characters, must not contain underscores, and must not contain uppercase letters.\n"
      }
    }
    • It’s preferred to structure the above validation into multiple separate validations:
    variable "name" {
      type = string
    
      validation {
        condition     = length(var.name) <= 64
        error_message = "The name must not exceed 64 characters."
      }
    
      validation {
        condition     = can(regex("^[^_]*$", var.name))
        error_message = "The name must not contain underscores."
      }
    
      validation {
        condition     = can(regex("^[a-z0-9]*$", var.name))
        error_message = "The name must not contain uppercase letters."
      }
    }

    Adopt a user-centric approach to Terraform and module development for users.

    • Clear & User-Friendly Messages: Strive for clarity in error messages to enhance user experience.
    • Guidance in Messages: Error messages should direct users on rectifying issues and, if possible, offer suggestions or acceptable values.
    • Informative Links: Including links to API, cloud platform, or Terraform documentation can provide users with easy access to crucial information and further assistance.

    Add validation limitations in individual variable description as well as in error messages.

    • Proactive Guidance: Clearly stating limitations in variable descriptions can guide users to provide correct inputs.
    • Automation & Documentation: When using tools like Terraform-docs for README.md creation, note that individual validation blocks aren’t populated in the generated documentation. However, variable descriptions are included. This is also true for adding modules to the Hashicorp Terraform Cloud Private Module Registry. Therefore, always integrating validation explanations in variable descriptions is crucial for comprehensive documentation.

    Test your own validations

    • Sometimes Terraform regex and other validation mechanisms can be difficult to ensure validity without an actual test. Luckily, it should be as easy as dropping a few incorrect values in a local module call and running a terraform plan or terraform validate against your validations.

    Avoid validation on arguments with requirements that may change in the future.

    • Validation Scope & Maintenance: For arguments in a single module, especially when requirements may change and apply to multiple resources and modules, like organizational regional limitations, prefer using broader code scanning tools like Hashicorp Sentinel. This approach minimizes the need for frequent updates to validations across different modules and resources in the future, ensuring that input validations are as resource-specific as possible, reducing the necessity of frequent updates across an organization or within a private module registry.

    Helpful Examples

    Data Type: Strings

    Length

    Validate from a List of Allowed Values

    Adding a condition with a list of allowed values is useful in many scenarios. For example, checking the VM size is as desired. In the 3 examples below, we
    check that the VM size is set to one of 3 values, “Standard_DS2”, “Standard_D2” or “Standard_DS2_v2” :

    variable "vm_size" {
     type = string
     description = "VM Size"
     validation {
     condition = anytrue([
     var.env == "Standard_DS2",
     var.env == "Standard_D2",
     var.env == "Standard_DS2_v2"
     ])
     error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
     }
    }

    Another way to achieve this without the anytrue function:

    variable "vm_size" {
     type = string
     description = "VM Size"
     validation {
     condition = contains(["Standard_DS2", "Standard_D2", "Standard_DS2_v2"], var.vm_size)
     error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
     } 
    }

    Another way with the OR operator:

    variable "vm_size" {
    type = string
    description = "VM Size"validation {
    condition = var.vm_size == "Standard_DS2" ||var.vm_size == "Standard_D2"||var.vm_size ==
    "Standard_DS2_v2"
    error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
    }
    }
    variable "vm_size" {
     type = string
     # using regex
     validation {
     condition = can(regex("^(Standard_DS2|Standard_D2)$", var.vm_size))
     error_message = "Invalid input, options: \"Standard_DS2\", \"Standard_D2\"."
     }
     # using contains()
     validation {
     condition = contains(["approved", "disapproved"], var.string_only_valid_options)
     error_message = "Invalid input, options: \"approved\", \"disapproved\"."
     }
    }

    Data Type: Numbers

    Number is Within a List of Allowed Values

    variable "allowed_num" {
      type = number
      validation {
        condition     = contains([1, 2, 5, 10], var.allowed_num)
        error_message = "The number must be one of the following: 1, 2, 5, 10."
      }
    }

    Number Must be Between a Range of Values

    variable "num_in_range" {
     type = number
     default = 1
     validation {
     condition = var.num_in_range >= 1 && var.num_in_range <= 16 && floor(var.num_in_range) == var.
    num_in_range
     error_message = "Accepted values: 1-16."
     }
    }

    Data Type: Lists

    Each entry in a list must start with an allowed list of prefixes

    variable "admin_group" {
     description = <<INFO
     List of users, groups, or service accounts to assign role of 
     roles/documentai.admin.
     This role has the following roles/permissions:
     roles/documentai.editor
     Must use the following format per member type:
     user:{email_id}
     group:{email_id}
     serviceAccount:{email_id}
     INFO
     type = list(string)
     default = []
     validation {
     condition = alltrue([
         for item in var.admin_group : (
         starts_with(item, "user:") || 
         starts_with(item, "group:") || 
         starts_with(item, "serviceAccount:")
       )
       ])
     error_message = "Each member in the admin_group must start with 'user:', 'group:', or 'serviceAccount:'."
     }
    

    Each entry in a list must be validated against a deny List

    variable "admin_group" {
     description = <<INFO
     List of users, groups, or service accounts to assign role of 
     roles/documentai.admin.
     This role has the following roles/permissions:
     roles/documentai.editor
     Must use the following format per member type:
     user:{email_id}
     group:{email_id}
     serviceAccount:{email_id}
     INFO
     type = list(string)
     default = []
    
    validation {
     condition = alltrue([
        for item in var.admin_group : true
        if alltrue([
        !contains(["allUsers", "allAuthenticatedUsers"], item)
       ])
      ])
     error_message = "The admin_group must not contain allUsers or allAuthenticatedUsers."
     }
    }

    Data Type: Maps, Objects, and Lists of Objects

    Validate a map with optional conflicting keys

    variable "only_one_optional_key" {
     type = object({
        name = optional(string)
        cidrs = optional(list(string))
        netmask = optional(number)
       })
     default = {
     cidr = "10.0.0.0/16"
     name = "test"
     }
     validation {
         error_message = "Can only specify either \"cidrs\", or \"netmask\"."
         condition = length(setintersection(keys(var.only_one_optional_key), ["cidrs", "netmask"])) == 1
     }

    List of Objects

    variable "index_fields" {
      type = list(object({
        field_path   = string
        order        = string
        array_config = string
      }))
      description = " The fields supported by this index. Refer https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/firestore_index#fields for details."
      validation {
        condition = length([
          for field in var.index_fields : true
          if contains(["CONTAINS"], field.array_config) || field.array_config == ""
        ]) == length(var.index_fields)
        error_message = "Array config must by one of the following values: CONTAINS."
      }
    
      validation {
        condition = length([
          for field in var.index_fields : true
          if contains(["ASCENDING", "DESCENDING"], field.order) || field.order == ""
        ]) == length(var.index_fields)
        error_message = "Order must by one of the following values: ASCENDING, DESCENDING."
      }
    
      validation {
        condition = length([
          for field in var.index_fields : true
          if (field.order == "" && field.array_config != "") || (field.order != "" && field.array_config == "")
        ]) == length(var.index_fields)
        error_message = "Only one of [order] or [array_config] must be provided."
      }
    }

    Unique Input Types

    IP Addresses

    variable "ip_address" {
     type = string
     description = "Example to validate IP address."
     validation {
     condition = can(regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
          \\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",var.ip_address))
     error_message = "Invalid IP address provided."
     }
    }
    

    Here we are using the can function which evaluates the regex expression and returns a boolean value indicating whether the expression produced a result without any errors.

    This would flag any invalid inputs such as :

    • “192.0.1.” (missing the last octet)
    • “192.65.0.256” (outside of 1–255)
    • “10.0.10,180” (comma instead of .)

    The following would validate a list of IP addresses:

    variable "ip_address_list" {
     type = string
     description = <<INFO
     Example to validate list of IP addresses.
     INFO
     validation {
     condition = can([for ip in var.ip_address_list: regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-
    5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-
    9]?)$", ip)])
     error_message = "Invalid List of IP addresses provided."
     }
    }
    

    Timestamps

    variable "timestamp" {
     type = string
     description = "Example to validate a timestamp" validation {
     condition = can(formatdate("", var.timestamp))
     error_message = "The timestamp argument requires a valid RFC 3339 timestamp."
     }
    }
    

    IPv4 CIDR

    variable "string_like_valid_ipv4_cidr" {
     type = string
     default = "10.0.0.0/16"
     validation {
     condition = can(cidrhost(var.string_like_valid_ipv4_cidr, 0))
     error_message = "Must be valid IPv4 CIDR."
     }
    }

    Semantic Versions

    variable "semv1" {
     default = "10.57.123"
     validation {
     error_message = "Must be valid semantic version."
     validation {
    condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-]
    [0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)
    *))?$", var.semv1))
     }
    }
    }

    Email Address

    variable "email_address" {
      type = string
      description = "Example to validate an email address."
      validation {
        condition     = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.email_address))
        error_message = "Invalid email address provided."
      }
    }
    

    Domain Name

    variable "domain_name" {
      type = string
      description = "Example to validate a domain name."
      validation {
        condition     = can(regex("^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$", var.domain_name))
        error_message = "Invalid domain name provided."
      }
    }

    Comments are closed