Terraform_testing
Terraform Testing
Section titled “Terraform Testing”Overview
Section titled “Overview”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.
Why Test Terraform?
Section titled “Why Test Terraform?”- 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
Types of Terraform Testing
Section titled “Types of Terraform Testing”┌─────────────────────────────────────────────────────────────────┐│ Terraform Testing Pyramid ││ ││ ▲ ││ │ ││ ┌───────────┴───────────┐ ││ │ Integration Tests │ (terraform plan/apply) ││ │ End-to-end testing │ ││ └───────────┬───────────┘ ││ │ ││ ┌───────────┴───────────┐ ││ │ Unit Tests │ (terraform test) ││ │ Terratest │ ││ └───────────┬───────────┘ ││ │ ││ ┌───────────┴───────────┐ ││ │ Static Analysis │ (terraform validate) ││ │ Linting │ (tflint) ││ └─────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Terraform Validate
Section titled “Terraform Validate”Basic syntax and validation:
# Validate configuration syntaxterraform validate
# Validate with inputterraform validate -json
# Check formattingterraform fmt -check -recursiveAdd to CI/CD:
# .gitlab-ci.yml or GitHub Actionsvalidate: script: - terraform fmt -check - terraform init - terraform validateTerratest
Section titled “Terratest”Terratest is a Go library for testing infrastructure:
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)}Testing with AWS
Section titled “Testing with AWS”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)}Testing with Kubernetes
Section titled “Testing with Kubernetes”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)}Terraform Test Command
Section titled “Terraform Test Command”Native testing (Terraform 1.5+):
mock_provider "aws" {}
run "test_case" { command = plan
assert { condition = aws_instance.test.id != "" error_message = "Instance ID should not be empty" }}Run tests:
terraform testterraform test -verboseterraform test -test-directory=./testsTFLint
Section titled “TFLint”Install and configure:
# Install TFLintbrew install tflint
# Initializetflint --init
# Run TFLinttflinttflint --moduletflint --format=compactTFLint Configuration
Section titled “TFLint Configuration”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" }}Custom Rules
Section titled “Custom Rules”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}Checkov
Section titled “Checkov”Infrastructure security scanning:
# Installpip install checkov
# Scan Terraformcheckov -d .checkov -d . --framework terraform
# Scan planterraform plan -out=tfplancheckov -f tfplan
# Output formatscheckov -d . -o sarifcheckov -d . -o json -f results.jsonCheckov Policies
Section titled “Checkov Policies”version: 2branch-filter-findings: []evaluation: omit-suppressions: falserun: all-checkov: true framework: - terraform skip-check: - CKV_AWS_1 - CKV_AWS_2 soft-fail: trueInfracost
Section titled “Infracost”Cost estimation testing:
# Installbrew install infracost
# Run with Terraforminfracost breakdown --path .infracost diff --path .Cost Checks
Section titled “Cost Checks”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 > 100Testing Best Practices
Section titled “Testing Best Practices”1. Test Structure
Section titled “1. Test Structure”├── main.tf├── variables.tf├── outputs.tf├── examples/│ └── simple/│ ├── main.tf│ ├── variables.tf│ └── outputs.tf└── tests/ ├── simple_test.go ├── fixtures/ │ └── main.tf └── testdata/2. Use Modules
Section titled “2. Use Modules”# Use tested modules instead of inline resourcesmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "3.0.0"
# ... tested module configuration}3. Remote State for Testing
Section titled “3. Remote State for Testing”terraform { backend "s3" { bucket = "terraform-test-state" key = "test/terraform.tfstate" }}4. Destroy Strategy
Section titled “4. Destroy Strategy”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)}CI/CD Integration
Section titled “CI/CD Integration”GitHub Actions
Section titled “GitHub Actions”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: .GitLab CI
Section titled “GitLab CI”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 .Summary
Section titled “Summary”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