Terraform
Infrastructure Testing with Terratest
Learn how to test your infrastructure code using Terratest to ensure reliability and prevent issues.
January 19, 2024
DevHub Team
4 min read
Infrastructure Testing with Terratest
Learn how to effectively test your infrastructure code using Terratest to ensure reliability and catch issues before they reach production.
Why Test Infrastructure Code?
- Catch configuration errors early
- Validate infrastructure behavior
- Ensure compliance and security
- Test infrastructure changes safely
- Automate validation processes
Getting Started with Terratest
Prerequisites
# Install Go brew install go # Install Terraform brew install terraform # Set up Go workspace mkdir -p ~/go/src/terraform-aws-example cd ~/go/src/terraform-aws-example
Project Structure
terraform-aws-example/ ├── main.tf ├── variables.tf ├── outputs.tf ├── test/ │ └── main_test.go └── go.mod
Basic Test Structure
// test/main_test.go package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" ) func TestTerraformBasicExample(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../", Vars: map[string]interface{}{ "region": "us-west-2", "instance_type": "t2.micro", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) }
Testing Different Resource Types
1. EC2 Instance Testing
func TestEC2Instance(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/ec2", Vars: map[string]interface{}{ "instance_type": "t2.micro", "ami_id": "ami-0c55b159cbfafe1f0", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) instanceID := terraform.Output(t, terraformOptions, "instance_id") aws.WaitForInstanceRunning(t, instanceID, "us-west-2") instanceType := aws.GetInstanceType(t, instanceID, "us-west-2") assert.Equal(t, "t2.micro", instanceType) }
2. VPC Testing
func TestVPCConfiguration(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/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") cidrBlock := aws.GetVpcCidrBlock(t, vpcID, "us-west-2") assert.Equal(t, "10.0.0.0/16", cidrBlock) }
3. S3 Bucket Testing
func TestS3Bucket(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/s3", Vars: map[string]interface{}{ "bucket_name": "test-bucket", "versioning_enabled": true, }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) bucketName := terraform.Output(t, terraformOptions, "bucket_name") versioning := aws.GetS3BucketVersioning(t, "us-west-2", bucketName) assert.Equal(t, "Enabled", versioning) }
Advanced Testing Patterns
1. HTTP Testing
func TestWebServer(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/web-server", } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) url := terraform.Output(t, terraformOptions, "url") http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello, World!", 30, 5*time.Second) }
2. Database Testing
func TestRDSInstance(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/rds", Vars: map[string]interface{}{ "db_name": "testdb", "db_user": "admin", "db_password": "password123", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) dbEndpoint := terraform.Output(t, terraformOptions, "db_endpoint") dbPort := terraform.Output(t, terraformOptions, "db_port") // Test database connection connectionString := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s", dbEndpoint, dbPort, "admin", "password123", "testdb", ) db, err := sql.Open("postgres", connectionString) assert.NoError(t, err) defer db.Close() err = db.Ping() assert.NoError(t, err) }
3. Load Balancer Testing
func TestLoadBalancer(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/alb", } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) albDNS := terraform.Output(t, terraformOptions, "alb_dns_name") // Test load balancer health http_helper.HttpGetWithRetryWithCustomValidation( t, fmt.Sprintf("http://%s", albDNS), nil, 30, 5*time.Second, func(status int, body string) bool { return status == 200 }, ) }
Testing Best Practices
1. Parallel Testing
func TestInParallel(t *testing.T) { t.Parallel() testCases := []struct { name string region string environment string }{ {"DevEnvironment", "us-west-2", "dev"}, {"StagingEnvironment", "us-east-1", "staging"}, {"ProdEnvironment", "eu-west-1", "prod"}, } for _, tc := range testCases { tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../", Vars: map[string]interface{}{ "region": tc.region, "environment": tc.environment, }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) }) } }
2. Retry Logic
func TestWithRetry(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ TerraformDir: "../examples/ec2", } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) instanceID := terraform.Output(t, terraformOptions, "instance_id") retry.DoWithRetry(t, "Check instance tags", 30, 5*time.Second, func() (string, error) { tags := aws.GetTagsForEc2Instance(t, instanceID, "us-west-2") if val, ok := tags["Environment"]; ok { return val, nil } return "", fmt.Errorf("Environment tag not found") }) }
3. Cleanup
func TestWithCleanup(t *testing.T) { t.Parallel() tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/cleanup-test") defer test_structure.CleanupTestDataFolder(t, tempTestFolder) terraformOptions := &terraform.Options{ TerraformDir: tempTestFolder, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) }
Integration with CI/CD
GitHub Actions
name: Terratest on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: '1.19' - name: Install Terraform uses: hashicorp/setup-terraform@v2 - name: Run Terratest run: | cd test go test -v ./... env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Test Coverage and Reporting
1. Test Coverage
# Generate test coverage report go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html
2. Test Output
func TestWithLogs(t *testing.T) { logger.Log(t, "Starting test...") terraformOptions := &terraform.Options{ TerraformDir: "../", Logger: logger.TestingT, } logger.Log(t, "Applying Terraform configuration...") terraform.InitAndApply(t, terraformOptions) logger.Log(t, "Test completed successfully") }
Conclusion
Effective infrastructure testing:
- Ensures reliability
- Prevents costly mistakes
- Enables confident changes
- Improves code quality
- Facilitates automation
Remember to:
- Write comprehensive tests
- Use proper cleanup
- Implement retry logic
- Test in parallel when possible
- Integrate with CI/CD pipelines
IaC
DevOps
Testing
Go