Skip to content

Terraform_testing

Testing Terraform configurations is crucial for ensuring infrastructure reliability, catching bugs early, and preventing misconfigurations in production. This guide covers various testing strategies for Terraform code.

  • Catch errors early: Detect issues before deployment
  • Validate changes: Ensure infrastructure changes work as expected
  • Prevent drift: Identify unexpected changes in state
  • Document behavior: Tests serve as documentation
  • Enable CI/CD: Automated testing in pipelines
┌─────────────────────────────────────────────────────────────────┐
│ Terraform Testing Pyramid │
│ │
│ ▲ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Integration Tests │ (terraform plan/apply) │
│ │ End-to-end testing │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Unit Tests │ (terraform test) │
│ │ Terratest │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Static Analysis │ (terraform validate) │
│ │ Linting │ (tflint) │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Basic syntax and validation:

Terminal window
# Validate configuration syntax
terraform validate
# Validate with input
terraform validate -json
# Check formatting
terraform fmt -check -recursive

Add to CI/CD:

# .gitlab-ci.yml or GitHub Actions
validate:
script:
- terraform fmt -check
- terraform init
- terraform validate

Terratest is a Go library for testing infrastructure:

test/terraform_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformExample(t *testing.T) {
// Configure Terraform options
terraformOptions := &terraform.Options{
TerraformDir: "../examples/simple",
Vars: map[string]interface{}{
"region": "us-east-1",
},
}
// Defer destroy to clean up
defer terraform.Destroy(t, terraformOptions)
// Apply Terraform
terraform.InitAndApply(t, terraformOptions)
// Get output
instanceID := terraform.Output(t, terraformOptions, "instance_id")
// Assert
assert.NotEmpty(t, instanceID)
}
func TestTerraformWithAWS(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "./aws-example",
Vars: map[string]interface{}{
"aws_region": "us-west-2",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Verify instance is running
instanceID := terraform.Output(t, terraformOptions, "instance_id")
// Use AWS SDK to verify
ec2Client := aws.NewEc2Client(t, "us-west-2")
instance := ec2Client.DescribeInstance(t, instanceID)
assert.Equal(t, "running", *instance.State.Name)
}
func TestTerraformKubernetes(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "./k8s-example",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get kubeconfig from terraform output
kubeconfig := terraform.Output(t, terraformOptions, "kubeconfig")
// Test Kubernetes resources
k8sClient := k8s.NewClient(t, kubeconfig)
pods := k8sClient.ListPods(t, "default")
assert.Greater(t, len(pods), 0)
}

Native testing (Terraform 1.5+):

tests/example.tftest.hcl
mock_provider "aws" {}
run "test_case" {
command = plan
assert {
condition = aws_instance.test.id != ""
error_message = "Instance ID should not be empty"
}
}

Run tests:

Terminal window
terraform test
terraform test -verbose
terraform test -test-directory=./tests

Install and configure:

Terminal window
# Install TFLint
brew install tflint
# Initialize
tflint --init
# Run TFLint
tflint
tflint --module
tflint --format=compact
.tflint.hcl
config {
module = true
force = false
}
plugin "aws" {
enabled = true
version = "0.29.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "aws_instance_invalid_type" {
enabled = true
}
rule "aws_instance_previous_type" {
enabled = true
}
rule "terraform_deprecated_interpolation" {
enabled = true
}
rule "terraform_naming_convention" {
enabled = true
variables {
naming_method = "snake_case"
}
}
rules/rules.go
package main
import (
"github.com/terraform-linters/tflint/tflint"
)
func Apply(c *tflint.Config) error {
c.ForEachRule("", func(r *tflint.Rule) error {
if r.Name == "custom_rule" {
r.Enabled = true
}
return nil
})
return nil
}

Infrastructure security scanning:

Terminal window
# Install
pip install checkov
# Scan Terraform
checkov -d .
checkov -d . --framework terraform
# Scan plan
terraform plan -out=tfplan
checkov -f tfplan
# Output formats
checkov -d . -o sarif
checkov -d . -o json -f results.json
.checkov.yaml
version: 2
branch-filter-findings: []
evaluation:
omit-suppressions: false
run:
all-checkov: true
framework:
- terraform
skip-check:
- CKV_AWS_1
- CKV_AWS_2
soft-fail: true

Cost estimation testing:

Terminal window
# Install
brew install infracost
# Run with Terraform
infracost breakdown --path .
infracost diff --path .
.infracost.yml
version: 0.1
projects:
- path: .
name: my-project
skip_comment_if_empty: true
breakdown:
aggregate_by: "sku"
diff:
show_percentage: true
checks:
- id: COST_BUDGET
message: "Monthly cost must not exceed $100"
threshold: 100
conditions:
- monthly_cost > 100
├── main.tf
├── variables.tf
├── outputs.tf
├── examples/
│ └── simple/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── tests/
├── simple_test.go
├── fixtures/
│ └── main.tf
└── testdata/
# Use tested modules instead of inline resources
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.0.0"
# ... tested module configuration
}
terraform {
backend "s3" {
bucket = "terraform-test-state"
key = "test/terraform.tfstate"
}
}
func TestTerraformExample(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "./example",
// Always force destroy to clean up
BackendConfig: map[string]interface{}{
"force_destroy": true,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
}
name: Terraform Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: TFLint
uses: terraform-linters/setup-tflint@v3
- name: Run TFLint
run: tflint --init && tflint
- name: Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: terraform
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Infracost
uses: infracost/infracost-github-action@v1
with:
path: .
stages:
- validate
- test
- plan
validate:
stage: validate
script:
- terraform fmt -check
- terraform init
- terraform validate
- tflint --init && tflint
test:
stage: test
script:
- go test -v ./tests/...
dependencies:
- validate
plan:
stage: plan
script:
- terraform plan -out=tfplan
- checkov -f tfplan
- infracost breakdown --path .

Testing Terraform is essential for:

  • Reliability: Ensure infrastructure works as expected
  • Security: Catch security issues early
  • Cost: Monitor and control infrastructure costs
  • CI/CD: Automate validation in pipelines

Key tools:

  • terraform validate: Basic syntax validation
  • Terratest: Go-based integration testing
  • TFLint: Linting and custom rules
  • Checkov: Security scanning
  • Infracost: Cost estimation