Validating input files with OpenTofu/Terraform

2025-02-11 - Works with JSON or YAML files
Tags: OpenTofu terraform

Introduction

I am used to building small abstraction layers over some OpenTofu/Terraform code via YAML input files. It would be too big an ask to require people (usually developers) unfamiliar with infrastructure automation to understand the intricacies of HCL, but filling up YAML (or JSON) files is no problem at all.

In this article I will explain how I perform some measure of validation on these input files, as well as handle default values.

Input file validation

I am using two nested modules to abstract this validation away. I name the top module input and its job is to read and decode the input files, then call the nested validation module with them.

Input module

A simplified version of this input module contains the following:

output "data" {
  description = "The output of the validation module."
  value       = module.validation
}

locals {
  input_path = "${path.module}/../../../inputs"
}

module "validation" {
  source = "./validation/"

  teams = yamldecode(file("${local.input_path}/teams.yaml"))
  users = yamldecode(file("${local.input_path}/users.yaml"))
}

There is a single output to expose the validated data. The input_path should obviously point to where your inputs data lives.

The validation submodule

The validation module does the heavy lifting of validating the input, handling default values and mangling data in necessary ways. Here is a simplified example:

output "aws_iam_users" {
  description = "The aws IAM users data."
  value = { for user, info in var.users :
    user => info if info.admin.aws
  }
}

output "users" {
  description = "The users data."
  value       = var.users
}

variable "users" {
  description = "The yaml decoded contents of the users input file."
  nullable    = false
  type = map(object({
    admin = optional(object({
      aws    = optional(bool, false)
      github = optional(bool, false)
    }), {})
    email  = string
    github = optional(string, null)
  }))
  validation {
    condition = alltrue([for _, info in var.users :
      endswith(info.email, "@adyxax.org")
    ])
    error_message = "A user's email must be for the @adyxax.org domain."
  }
}

Here I have two outputs: one that mangles the input data a bit to filter AWS admin users, and another that simply returns the input data augmented by the default values. I added a validation block that checks that every users’ email address is on the proper domain.

Usage

Using this input module is as simple as:

module "input" {
  source = "../modules/input/"
}

With this, you can then do something with module.input.data.users or module.input.data.aws_iam_users. A common debugging step can be to run OpenTofu or Terraform with the console command and inspect the resulting input data.

Limitations

The main limitation of this validation system is that invalid (or misspelled) keys in the original input file are simply ignored by OpenTofu/Terraform. I did not find a way around it with just terraform which is frustrating!

A solution to this particular need that relies on outside tooling is to perform JSON schema or YAML schema validation. This solves the problem and runs nicely in a CI environment.

Conclusion

This pattern is really useful, use it without moderation!