Infrastructure Testing with Terratest
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?

  1. Catch configuration errors early
  2. Validate infrastructure behavior
  3. Ensure compliance and security
  4. Test infrastructure changes safely
  5. 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:

  1. Write comprehensive tests
  2. Use proper cleanup
  3. Implement retry logic
  4. Test in parallel when possible
  5. Integrate with CI/CD pipelines
IaC
DevOps
Testing
Go