DEV Community

Mariusz Gębala
Mariusz Gębala

Posted on • Originally published at haitmg.pl

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
}
Enter fullscreen mode Exit fullscreen mode

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-')."
  }
}
Enter fullscreen mode Exit fullscreen mode

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."
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)