Infrastructure Testing: Validating Your IaC Before Production
February 16, 2026 · 6 min · 1115 words · Rob Washington
Table of Contents
You test your application code. Why not your infrastructure code?
Infrastructure as Code (IaC) has the same failure modes as any software: bugs, regressions, unintended side effects. Yet most teams treat Terraform and Ansible like configuration files rather than code that deserves tests.
# conftest policy (Open Policy Agent)# policy/terraform.regopackageterraformdeny[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==22msg:="SSH open to the world is not allowed"}deny[msg]{resource:=input.resource_changes[_]resource.type=="aws_instance"notstartswith(resource.change.after.instance_type,"t3.")msg:=sprintf("Instance type %s not allowed, use t3 family",[resource.change.after.instance_type])}
1
2
3
4
# Run policy against planterraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json
packagetestimport("testing""github.com/gruntwork-io/terratest/modules/terraform""github.com/gruntwork-io/terratest/modules/aws""github.com/stretchr/testify/assert")funcTestVpcModule(t*testing.T){t.Parallel()terraformOptions:=&terraform.Options{TerraformDir:"../modules/vpc",Vars:map[string]interface{}{"vpc_cidr":"10.0.0.0/16","environment":"test","az_count":2,},}// Clean up after testdeferterraform.Destroy(t,terraformOptions)// Deploy infrastructureterraform.InitAndApply(t,terraformOptions)// Get outputsvpcId:=terraform.Output(t,terraformOptions,"vpc_id")publicSubnets:=terraform.OutputList(t,terraformOptions,"public_subnet_ids")privateSubnets:=terraform.OutputList(t,terraformOptions,"private_subnet_ids")// Validate VPC existsvpc:=aws.GetVpcById(t,vpcId,"us-east-1")assert.Equal(t,"10.0.0.0/16",vpc.CidrBlock)// Validate subnet countassert.Equal(t,2,len(publicSubnets))assert.Equal(t,2,len(privateSubnets))// Validate subnets are in different AZsfori,subnetId:=rangepublicSubnets{subnet:=aws.GetSubnetById(t,subnetId,"us-east-1")assert.Contains(t,subnet.AvailabilityZone,fmt.Sprintf("us-east-1%c",'a'+i))}}
funcTestWebServer(t*testing.T){t.Parallel()terraformOptions:=&terraform.Options{TerraformDir:"../modules/web-server",}deferterraform.Destroy(t,terraformOptions)terraform.InitAndApply(t,terraformOptions)// Get the public URLurl:=terraform.Output(t,terraformOptions,"public_url")// Retry with backoff - infrastructure needs time to stabilizemaxRetries:=30timeBetweenRetries:=10*time.Secondhttp_helper.HttpGetWithRetry(t,url,nil,200,"Welcome",maxRetries,timeBetweenRetries,)}
funcTestModule1(t*testing.T){t.Parallel()// This test runs concurrently// ...}funcTestModule2(t*testing.T){t.Parallel()// This test runs concurrently// ...}
// Use smallest instance typesVars:map[string]interface{}{"instance_type":"t3.micro","min_size":1,"max_size":1,}// Always destroy, even on failuredeferterraform.Destroy(t,terraformOptions)// Set timeouts to avoid runaway coststerraformOptions.MaxRetries=3terraformOptions.TimeBetweenRetries=5*time.Second
Infrastructure tests answer the question: “If I apply this change, will production still work?”
Static analysis catches typos and policy violations. Plan testing catches logical errors. Integration testing catches runtime issues.
You wouldn’t ship application code without tests. Your infrastructure deserves the same discipline.
The cost of testing is hours and cloud spend. The cost of not testing is a 3 AM incident when that security group rule you didn’t review takes down production.
Test your infrastructure. Sleep better.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.