· 3 min read

Why Every Terraform Module Needs Proper Validation

terraformiacawsbest-practices
HAIT Cloud & DevOps Consulting

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:

WhatWhy
Non-empty required listsPrevents silent no-ops
ID format (subnet-, vpc-, sg-)Catches copy-paste errors
CIDR block formatRegex validation on network inputs
Mutually exclusive flagse.g., single_nat_gateway vs per-AZ mode
Cross-variable consistencyPreconditions 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.