# Terraform Testing Strategies ## Terraform Plan Validation **Basic Plan Workflow** ```bash # Initialize and validate syntax terraform init terraform fmt -check terraform validate # Plan with output terraform plan -out=tfplan # Show plan in JSON for automated review terraform show -json tfplan | jq . # Apply specific plan terraform apply tfplan ``` **Plan with Variable Files** ```bash # Plan with specific tfvars terraform plan -var-file="production.tfvars" # Plan with inline variables terraform plan -var="instance_count=5" # Plan with multiple var files terraform plan \ -var-file="common.tfvars" \ -var-file="production.tfvars" ``` **Plan Analysis** ```bash # Resource targeting for specific resources terraform plan -target=aws_vpc.main # Refresh only (check drift) terraform plan -refresh-only # Destroy plan terraform plan -destroy # Save plan output terraform plan -out=tfplan 2>&1 | tee plan-output.txt ``` ## Terraform Test (1.6+) **Test File Structure** ``` tests/ ├── unit/ │ ├── vpc_test.tftest.hcl │ └── security_group_test.tftest.hcl └── integration/ └── complete_test.tftest.hcl ``` **Basic Test** ```hcl # tests/vpc_test.tftest.hcl run "validate_vpc_cidr" { command = plan variables { cidr_block = "10.0.0.0/16" name = "test-vpc" } assert { condition = aws_vpc.main.cidr_block == "10.0.0.0/16" error_message = "VPC CIDR block did not match expected value" } assert { condition = aws_vpc.main.enable_dns_hostnames == true error_message = "DNS hostnames should be enabled" } } run "validate_tags" { command = plan variables { cidr_block = "10.0.0.0/16" name = "test-vpc" tags = { Environment = "test" } } assert { condition = aws_vpc.main.tags["Environment"] == "test" error_message = "Environment tag not set correctly" } } ``` **Integration Test** ```hcl # tests/integration/complete_test.tftest.hcl run "create_full_stack" { command = apply variables { cidr_block = "10.0.0.0/16" name = "integration-test" private_subnets = { app = { cidr_block = "10.0.1.0/24", az = "us-east-1a" } } } assert { condition = length(aws_subnet.private) == 1 error_message = "Should create exactly one private subnet" } assert { condition = output.vpc_id != "" error_message = "VPC ID should not be empty" } } ``` **Run Tests** ```bash # Run all tests terraform test # Run specific test file terraform test tests/vpc_test.tftest.hcl # Verbose output terraform test -verbose # Keep test resources (for debugging) terraform test -no-cleanup ``` ## Terratest (Go-based Testing) **Test Structure** ``` tests/ ├── go.mod ├── go.sum └── vpc_test.go ``` **go.mod** ```go module github.com/example/terraform-modules/tests go 1.21 require ( github.com/gruntwork-io/terratest v0.45.0 github.com/stretchr/testify v1.8.4 ) ``` **Basic Terratest** ```go // tests/vpc_test.go package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" ) func TestVPCCreation(t *testing.T) { t.Parallel() terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ TerraformDir: "../examples/complete", Vars: map[string]interface{}{ "name": "test-vpc", "cidr_block": "10.0.0.0/16", }, EnvVars: map[string]string{ "AWS_DEFAULT_REGION": "us-east-1", }, }) defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) vpcID := terraform.Output(t, terraformOptions, "vpc_id") assert.NotEmpty(t, vpcID) vpcCIDR := terraform.Output(t, terraformOptions, "vpc_cidr_block") assert.Equal(t, "10.0.0.0/16", vpcCIDR) } ``` **Advanced Terratest with AWS SDK** ```go package test import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/gruntwork-io/terratest/modules/terraform" aws_helper "github.com/gruntwork-io/terratest/modules/aws" "github.com/stretchr/testify/assert" ) func TestVPCConfiguration(t *testing.T) { t.Parallel() awsRegion := "us-east-1" terraformOptions := &terraform.Options{ TerraformDir: "../examples/complete", Vars: map[string]interface{}{ "name": "test-vpc", "cidr_block": "10.0.0.0/16", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) vpcID := terraform.Output(t, terraformOptions, "vpc_id") // Verify VPC configuration using AWS SDK vpc := aws_helper.GetVpcById(t, vpcID, awsRegion) assert.Equal(t, "10.0.0.0/16", *vpc.CidrBlock) assert.True(t, *vpc.EnableDnsSupport) assert.True(t, *vpc.EnableDnsHostnames) // Verify tags tags := convertEC2TagsToMap(vpc.Tags) assert.Equal(t, "test-vpc", tags["Name"]) } func convertEC2TagsToMap(tags []*ec2.Tag) map[string]string { result := make(map[string]string) for _, tag := range tags { result[*tag.Key] = *tag.Value } return result } ``` **Run Terratest** ```bash cd tests go mod download go test -v -timeout 30m ``` ## Policy as Code - OPA/Sentinel **Open Policy Agent (OPA)** **policy.rego** ```rego package terraform.analysis import input as tfplan # Deny if resources are not tagged deny[msg] { r := tfplan.resource_changes[_] r.change.actions[_] == "create" not r.change.after.tags.Environment msg := sprintf("Resource %s is missing Environment tag", [r.address]) } # Require encryption for S3 buckets deny[msg] { r := tfplan.resource_changes[_] r.type == "aws_s3_bucket" r.change.actions[_] == "create" not r.change.after.server_side_encryption_configuration msg := sprintf("S3 bucket %s must have encryption enabled", [r.address]) } # Ensure VPC flow logs are enabled deny[msg] { r := tfplan.resource_changes[_] r.type == "aws_vpc" r.change.actions[_] == "create" vpc_id := r.change.after.id not has_flow_log(vpc_id) msg := sprintf("VPC %s must have flow logs enabled", [r.address]) } has_flow_log(vpc_id) { r := tfplan.resource_changes[_] r.type == "aws_flow_log" r.change.after.vpc_id == vpc_id } ``` **Run OPA Policy** ```bash # Generate plan in JSON terraform plan -out=tfplan terraform show -json tfplan > tfplan.json # Run OPA policy check opa eval -i tfplan.json -d policy.rego "data.terraform.analysis.deny" ``` **Conftest (OPA wrapper for testing)** ```bash # Install conftest brew install conftest # Test plan against policies conftest test tfplan.json # With specific namespace conftest test tfplan.json --namespace terraform.analysis ``` ## TFLint **Installation and Configuration** ```bash # Install tflint brew install tflint # Initialize tflint plugins tflint --init ``` **.tflint.hcl** ```hcl plugin "terraform" { enabled = true preset = "recommended" } plugin "aws" { enabled = true version = "0.27.0" source = "github.com/terraform-linters/tflint-ruleset-aws" } rule "terraform_naming_convention" { enabled = true format = "snake_case" } rule "terraform_required_version" { enabled = true } rule "terraform_required_providers" { enabled = true } rule "aws_instance_invalid_type" { enabled = true } rule "aws_s3_bucket_encryption" { enabled = true } ``` **Run TFLint** ```bash # Run linter tflint # With specific config tflint --config=.tflint.hcl # Recursive (all subdirectories) tflint --recursive # Output format tflint --format=json ``` ## Pre-commit Hooks **.pre-commit-config.yaml** ```yaml repos: - repo: https://github.com/antonbabenko/pre-commit-terraform rev: v1.83.6 hooks: - id: terraform_fmt - id: terraform_validate - id: terraform_tflint args: - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl - id: terraform_docs args: - --hook-config=--path-to-file=README.md - --hook-config=--add-to-existing-file=true - id: terraform_checkov args: - --args=--quiet - --args=--skip-check CKV_AWS_* ``` **Setup** ```bash # Install pre-commit pip install pre-commit # Install hooks pre-commit install # Run manually pre-commit run -a ``` ## CI/CD Pipeline Testing **GitHub Actions Example** ```yaml name: Terraform Test on: [pull_request] jobs: terraform-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 with: terraform_version: 1.6.0 - 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 with: tflint_version: latest - name: Run TFLint run: tflint --recursive - name: Terraform Test run: terraform test - name: Checkov uses: bridgecrewio/checkov-action@master with: directory: . framework: terraform ``` ## Best Practices - Run `terraform validate` before every commit - Use `terraform test` for unit and integration tests - Implement policy as code for security compliance - Run TFLint in CI/CD pipelines - Use pre-commit hooks for automated checks - Test modules with Terratest for critical infrastructure - Always review plan output before apply - Test provider upgrades in isolated environments - Document test scenarios and expected outcomes - Automate testing in pull request workflows