Table of Contents

Why Test Terraform Code?

Infrastructure bugs are expensive. A misconfigured security group can expose your database. A wrong CIDR can break networking. Testing catches these before they hit production.

Level 1: Built-in Validation

Variable Validation Rules

variable "instance_type" {
  type = string
  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

variable "environment" {
  type = string
  validation {
    condition     = can(regex("^(dev|staging|prod)$", var.environment))
    error_message = "Environment must be dev, staging, or prod."
  }
}

Preconditions and Postconditions

resource "aws_instance" "web" {
  instance_type = var.instance_type
  ami           = data.aws_ami.ubuntu.id

  lifecycle {
    precondition {
      condition     = data.aws_ami.ubuntu.architecture == "x86_64"
      error_message = "AMI must be x86_64 architecture."
    }

    postcondition {
      condition     = self.public_ip != ""
      error_message = "Instance must have a public IP."
    }
  }
}

Level 2: terraform test (Native Testing)

# tests/vpc_test.tftest.hcl
run "create_vpc" {
  command = plan

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR should be 10.0.0.0/16"
  }

  assert {
    condition     = aws_vpc.main.enable_dns_hostnames == true
    error_message = "DNS hostnames should be enabled"
  }
}

Run with: terraform test

Level 3: Terratest (Go-Based Integration Tests)

func TestVpcModule(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "vpc_cidr":    "10.0.0.0/16",
            "environment": "test",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)
}

Level 4: Policy as Code

Open Policy Agent (OPA)

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_security_group_rule"
    resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
    resource.change.after.from_port == 22
    msg := "SSH must not be open to the world"
}

Testing Strategy

LevelToolSpeedCostWhen
Validationvariable validationInstantFreeEvery plan
Staticterraform validate, tflintSecondsFreeEvery commit
Unitterraform test (plan)SecondsFreeEvery PR
IntegrationTerratestMinutesCloud costsPre-merge
PolicyOPA, SentinelSecondsFreeEvery plan

Learn More