Search code examples
validationterraformhcl

Can I use condition's variable in the error_message of a Terraform variable validation?


tl;dr: I want to use the current value of a runtime variable created in the condition part of a variable validation, to give more details about the error.

I am looking for a way to improve my error_message for variable validation in my Terraform project.

Say I have 2 variable:

  • availabilityzones gives the list of AZ my module/project can use.
  • workloads defines some workload to start for this module/project can use.

Workloads have an AMI (not relevant here) and are given a number of AZ they can use (span): az_count.

My workloads variable has a validation to make sure the number of AZ is consistent with the size of availabilityzones:

condition = alltrue([
  for workload_name, workload in var.workloads :
    1 <= workload.az_count && workload.az_count <= length(var.availabilityzones)
])

Validation works and can detect too large az_count, but I would like to tell the user which workload caused the issue, so I tried to use the workload_name variable (the one from the condition) in the error_message:

error_message = "For a workload, the AZ count must be between `1` and the number of configured AZ (both inclusive). Workload \"${workload_name}\" has: ${tonumber(workload.az_count)}"

But it fails:

│ Error: Invalid reference
│ 
│   on …/variables.tf line 42, in variable "workloads":
│  129:     error_message = "For a workload, the AZ count must be between `1` and the number of configured AZ (both inclusive). Workload \"${workload_name}\" has: ${tonumber(workload.az_count)}"
│ 
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.

I am guessing the context from condition is lost once validation has detected an error, and it not passed to error_message's context?

Is there a workaround?

One I can think of would be repeating the test in error_message to re-find the invalid value, but this would always find/display the first invalid value, even if multiple workloads are incorrect.

Full code below:

variable "availabilityzones" {
  description = "The availability zones (AZs) to use."
  default = [
    "eu-west-1a",
    "eu-west-1b",
    "eu-west-1c",
  ]
  type = set(string)
}

variable "workloads" {
  default = {
    workload_foo = {
      az_count = 1
      ami = "ami-061ff9349538cd3f1"
    }
    workload_bar = {
      az_count = 2
      ami = "ami-0e309138dd3e25b2e"
    }
    workload_bad = {
      az_count = 4 # ⟽ Too many AZ here
      ami = "ami-040a86d5d7bf29267"
    }
  }
  type = map(object({
      # How many AZ to use?
      az_count = number

      # Which AMI should the workload use?
      ami = string
    }))

  validation {
    condition = alltrue([
      for workload_name, workload in var.workloads :
        1 <= workload.az_count && workload.az_count <= length(var.availabilityzones)
    ])
    error_message = "For a workload, the AZ count must be between `1` and the number of configured AZ (both inclusive). Workload \"${workload_name}\" has: ${tonumber(workload.az_count)}"
  }
}

Solution

  • I don't think you'll be able to use a validation block for this given that within them you can only refer to the variable context.

    You can however achieve the desired behavior with a check block:

    variable "availabilityzones" {
      description = "The availability zones (AZs) to use."
      default = [
        "eu-west-1a",
        "eu-west-1b",
        "eu-west-1c",
      ]
      type = set(string)
    }
    
    variable "workloads" {
      default = {
        workload_foo = {
          az_count = 1
          ami      = "ami-061ff9349538cd3f1"
        }
        workload_bar = {
          az_count = 2
          ami      = "ami-0e309138dd3e25b2e"
        }
        workload_bad = {
          az_count = 4 # ⟽ Too many AZ here
          ami      = "ami-040a86d5d7bf29267"
        }
      }
      type = map(object({
        # How many AZ to use?
        az_count = number
    
        # Which AMI should the workload use?
        ami = string
      }))
    
    }
    
    locals {
      workflow_check = {
        for workload_name, workload in var.workloads :
        workload_name => [1 <= workload.az_count && workload.az_count <= length(var.availabilityzones), workload.az_count]
      }
      invalid_workflow = {for k,v in local.workflow_check : k => tostring(v[1]) if v[0] == false}
    }
    
    check "validate_variables" {
      assert {
        condition = local.invalid_workflow == {}
        error_message = local.invalid_workflow == {} ? "" : "For a workload, the AZ count must be between `1` and the number of configured AZ (both inclusive). Workload \"${keys(local.invalid_workflow)[0]}\" has: ${tonumber(values(local.invalid_workflow)[0])}"
      }
    }
    
    $ terraform plan
    ╷
    │ Warning: Check block assertion failed
    │ 
    │   on main.tf line 49, in check "validate_variables":
    │   49:     condition = local.invalid_workflow == {}
    │     ├────────────────
    │     │ local.invalid_workflow is object with 1 attribute "workload_bad"
    │ 
    │ For a workload, the AZ count must be between `1` and the number of configured AZ (both inclusive). Workload "workload_bad" has: 4
    ╵
    

    you can adapt this so that in the case that there are multiple workloads that fail the check the error message includes them all.