Why Every Terraform Module Needs Proper Validation
If you’ve ever deployed a Terraform module only to discover that someone passed a private subnet ID where a public one was expected, you know the pain. The deployment “succeeds”, but nothing works. You spend 30 minutes debugging, only to realize the input was wrong from the start.
Terraform has tools to prevent this. Most people don’t use them.
The Problem: Silent Misconfiguration
Consider a simple NAT Gateway module:
variable "subnet_id" {
description = "Subnet to place the NAT Gateway in"
type = string
}
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.this.id
subnet_id = var.subnet_id
}
This accepts any subnet ID. Public, private, doesn’t matter. Terraform won’t complain. AWS won’t complain (immediately). But your private subnets won’t have internet access, and you’ll spend time figuring out why.
The Fix: Validation Blocks
Since Terraform 1.0, you can add validation blocks to variables:
variable "public_subnet_ids" {
description = "Public subnet IDs for NAT Gateway placement"
type = list(string)
validation {
condition = length(var.public_subnet_ids) > 0
error_message = "At least one public subnet ID is required."
}
validation {
condition = alltrue([for id in var.public_subnet_ids : startswith(id, "subnet-")])
error_message = "All values must be valid subnet IDs (starting with 'subnet-')."
}
}
Now terraform plan fails immediately with a clear message if someone passes an empty list or garbage values.
Going Further: Preconditions
For validations that need to check relationships between variables, use precondition blocks in lifecycle:
resource "aws_nat_gateway" "this" {
count = var.single_nat_gateway ? 1 : length(var.public_subnet_ids)
allocation_id = aws_eip.this[count.index].id
subnet_id = var.public_subnet_ids[count.index]
lifecycle {
precondition {
condition = var.single_nat_gateway || length(var.public_subnet_ids) >= length(var.private_route_table_ids)
error_message = "When using multi-AZ NAT, you need at least as many public subnets as private route tables."
}
}
}
This catches architectural mistakes at plan time, not after a 10-minute apply.
What I Validate in Every Module
After building 12 Terraform modules for AWS, here’s my checklist:
| What | Why |
|---|---|
| Non-empty required lists | Prevents silent no-ops |
ID format (subnet-, vpc-, sg-) | Catches copy-paste errors |
| CIDR block format | Regex validation on network inputs |
| Mutually exclusive flags | e.g., single_nat_gateway vs per-AZ mode |
| Cross-variable consistency | Preconditions on resource blocks |
The Payoff
Every validation you add is one fewer support ticket, one fewer “why isn’t this working” Slack message, and one fewer hour lost to debugging obvious misconfigurations.
The best part: these validations run during terraform plan. Zero cost. Zero risk. Just faster feedback.
Building Terraform modules for AWS? Check out the HAIT module collection on the Terraform Registry.