aboutsummaryrefslogtreecommitdiff
path: root/content/blog/terraform/input_validation.md
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/blog/terraform/input_validation.md122
1 files changed, 122 insertions, 0 deletions
diff --git a/content/blog/terraform/input_validation.md b/content/blog/terraform/input_validation.md
new file mode 100644
index 0000000..b352304
--- /dev/null
+++ b/content/blog/terraform/input_validation.md
@@ -0,0 +1,122 @@
+---
+title: 'Validating JSON or YAML input files with terraform'
+description: 'a much anticipated feature'
+date: '2025-02-11'
+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:
+
+``` hcl
+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:
+
+``` hcl
+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:
+
+``` hcl
+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!