Pruebas de Infraestructura con Terratest
Terratest is a Go library for testing infrastructure code by actually provisioning infrastructure and validating it works correctly. Using Terratest, you write automated tests for Terraform, CloudFormation, Kubernetes, and Docker, catching infrastructure bugs before production deployment. This guide covers Go setup, writing infrastructure tests, using Terraform helpers, HTTP testing, retry logic, cleanup patterns, and CI/CD integration.
Tabla de Contenidos
- Terratest Overview
- Go Environment Setup
- Basic Terratest Structure
- Terraform Testing
- HTTP and Network Testing
- SSH Testing
- Retry and Polling
- Cleanup and Resource Management
- CI/CD Integration
- Conclusion
Descripción General de Terratest
Terratest enables testing infrastructure code by provisioning actual resources in test environments. Unlike unit tests, infrastructure tests validate that code works in real cloud environments, catching configuration mistakes and integration issues early.
Key benefits:
- Real Testing: Test actual infrastructure, not mocked resources
- Automation: Automated validation reduces manual testing
- Early Detection: Catch bugs before production
- Documentation: Tests document how infrastructure should work
- Regression Prevention: Catch breaking changes
- Confidence: Know your infrastructure code works
Terratest workflow:
Test Code
│
├── Provision Infrastructure (Terraform/CloudFormation)
│ │
│ └── Create real resources in test environment
│
├── Test Infrastructure
│ │
│ ├── HTTP requests
│ ├── SSH connections
│ ├── Resource validation
│ └── Integration testing
│
├── Validate Results
│ │
│ └── Assert expected behavior
│
└── Cleanup
│
└── Destroy test resources
Configuración del Entorno Go
Install Go and configure Terratest.
Install Go:
# macOS
brew install go
# Ubuntu/Debian
wget https://go.dev/dl/go1.20.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
# Windows
choco install golang
# Verify
go version
Initialize Go module:
# Create test directory
mkdir terraform-tests
cd terraform-tests
# Initialize go module
go mod init terraform-tests
# Add Terratest dependency
go get -u github.com/gruntwork-io/terratest
go get -u github.com/gruntwork-io/terratest/modules/terraform
go get -u github.com/gruntwork-io/terratest/modules/http-helper
Project structure:
terraform-tests/
├── go.mod
├── go.sum
├── test/
│ ├── terraform_test.go
│ └── integration_test.go
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── Makefile
Estructura Básica de Terratest
Create and run basic infrastructure tests.
Simple test file:
// 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) {
// Terraform options
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
Vars: map[string]interface{}{
"environment": "test",
"region": "us-east-1",
},
}
// Cleanup after test
defer terraform.Destroy(t, terraformOptions)
// Apply infrastructure
terraform.InitAndApply(t, terraformOptions)
// Get outputs
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
subnetId := terraform.Output(t, terraformOptions, "subnet_id")
// Validate outputs exist
assert.NotEmpty(t, vpcId, "VPC ID should not be empty")
assert.NotEmpty(t, subnetId, "Subnet ID should not be empty")
}
Run tests:
# Run specific test
go test -v test/terraform_test.go -timeout 30m
# Run all tests
go test -v ./test/... -timeout 30m
# Run with parallel execution
go test -v -parallel 2 ./test/... -timeout 30m
# Run specific test function
go test -v -run TestTerraformExample ./test/...
Makefile for testing:
# Makefile
.PHONY: test test-verbose test-short clean
test:
go test -v -timeout 30m ./test/...
test-verbose:
go test -v -timeout 30m ./test/... -count=1
test-short:
go test -short -timeout 5m ./test/...
clean:
find terraform -name '.terraform' -type d -exec rm -rf {} + 2>/dev/null || true
find terraform -name 'terraform.tfstate*' -delete
find terraform -name '.terraform.lock.hcl' -delete
test-clean: clean test
Pruebas de Terraform
Test Terraform configurations comprehensively.
Configuration testing:
// test/terraform_config_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTerraformPlan(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
// Just run plan, don't apply
plan := terraform.InitAndPlan(t, terraformOptions)
// Verify plan output
assert.NotEmpty(t, plan)
assert.Contains(t, plan, "Plan:")
}
func TestTerraformValidate(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
// Validate syntax and configuration
terraform.Init(t, terraformOptions)
err := terraform.Validate(t, terraformOptions)
require.NoError(t, err, "Terraform validation should succeed")
}
func TestTerraformApplyIdempotent(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
Vars: map[string]interface{}{
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
// First apply
terraform.InitAndApply(t, terraformOptions)
// Second apply should detect no changes
secondApply := terraform.Plan(t, terraformOptions)
assert.Contains(t, secondApply, "No changes")
}
func TestTerraformVariables(t *testing.T) {
tests := map[string]struct {
vars map[string]interface{}
error bool
}{
"valid": {
vars: map[string]interface{}{
"instance_type": "t2.micro",
"environment": "test",
},
error: false,
},
"invalid_instance_type": {
vars: map[string]interface{}{
"instance_type": "invalid-type",
},
error: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
Vars: tc.vars,
}
_, err := terraform.InitAndApplyE(t, terraformOptions)
if tc.error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
terraform.Destroy(t, terraformOptions)
}
})
}
}
Resource validation:
// test/terraform_aws_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAWSResources(t *testing.T) {
awsRegion := "us-east-1"
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
Vars: map[string]interface{}{
"aws_region": awsRegion,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get VPC ID from Terraform output
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
// Validate VPC exists
vpc := aws.GetVpcById(t, vpcId, awsRegion)
assert.NotNil(t, vpc)
assert.Equal(t, "10.0.0.0/16", *vpc.CidrBlock)
// Check security group
sgId := terraform.Output(t, terraformOptions, "security_group_id")
sg := aws.GetSecurityGroupById(t, sgId, awsRegion)
assert.NotNil(t, sg)
assert.Equal(t, 1, len(sg.IpPermissions))
// Verify instance
instanceId := terraform.Output(t, terraformOptions, "instance_id")
reservation := aws.GetEC2InstanceById(t, instanceId, awsRegion)
assert.NotNil(t, reservation)
assert.Equal(t, "running", *reservation.Instances[0].State.Name)
}
Pruebas HTTP y de Red
Test infrastructure accessibility via HTTP and network protocols.
HTTP endpoint testing:
// test/http_test.go
package test
import (
"fmt"
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestHTTPEndpoint(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get server URL
url := terraform.Output(t, terraformOptions, "server_url")
// Test HTTP endpoint
expectedStatus := 200
expectedBody := "Hello World"
http_helper.HttpGetWithValidationE(
t,
url,
nil,
expectedStatus,
expectedBody,
)
}
func TestHTTPWithRetry(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
url := terraform.Output(t, terraformOptions, "server_url")
// Retry logic for eventual consistency
maxRetries := 30
timeBetweenRetries := time.Second
http_helper.HttpGetWithRetry(
t,
fmt.Sprintf("%s/health", url),
nil,
200,
"ok",
maxRetries,
timeBetweenRetries,
)
}
func TestAPIEndpoint(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
apiUrl := terraform.Output(t, terraformOptions, "api_url")
// Test API with custom validation
statusCode, body := http_helper.HttpGet(t, apiUrl, nil)
assert.Equal(t, 200, statusCode)
assert.Contains(t, body, "data")
}
Pruebas SSH
Connect to instances and validate configuration via SSH.
SSH connection testing:
// test/ssh_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/ssh"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSSHConnection(t *testing.T) {
awsRegion := "us-east-1"
keyPairName := "test-key"
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
Vars: map[string]interface{}{
"key_name": keyPairName,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get instance details
instanceId := terraform.Output(t, terraformOptions, "instance_id")
instance := aws.GetEC2Instance(t, instanceId, awsRegion)
// Get public IP
publicIp := instance.PublicIpAddress
// Create SSH host
host := ssh.Host{
Hostname: publicIp,
SshUserName: "ec2-user",
SshKeyPair: getSSHKey(t, keyPairName),
}
// Test SSH connection
cmd := "echo Hello from EC2"
output := ssh.CheckSshCommand(t, host, cmd)
assert.Contains(t, output, "Hello from EC2")
}
func TestServerConfiguration(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get instance and SSH host
instanceId := terraform.Output(t, terraformOptions, "instance_id")
instance := aws.GetEC2Instance(t, instanceId, "us-east-1")
host := ssh.Host{
Hostname: instance.PublicIpAddress,
SshUserName: "ubuntu",
SshKeyPair: loadPrivateKey(t),
}
// Verify Nginx installed
assert.NoError(t,
ssh.CheckSshCommandE(t, host, "which nginx"))
// Verify Nginx running
output := ssh.CheckSshCommand(t, host, "sudo systemctl is-active nginx")
assert.Equal(t, "active", output)
// Check application process
assert.NoError(t,
ssh.CheckSshCommandE(t, host, "ps aux | grep myapp"))
}
func getSSHKey(t *testing.T, keyName string) *ssh.KeyPair {
// Load SSH key from file or AWS
// Implementation depends on how keys are stored
return &ssh.KeyPair{}
}
func loadPrivateKey(t *testing.T) *ssh.KeyPair {
// Load from file
keypair, err := ssh.NewKeyPairE("~/.ssh/id_rsa")
require.NoError(t, err)
return keypair
}
Reintentos y Sondeo
Handle eventual consistency and transient failures.
Retry patterns:
// test/retry_test.go
package test
import (
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/retry"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestWithRetry(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Define retry logic
maxRetries := 10
timeBetweenRetries := time.Second * 5
err := retry.DoWithRetry(
t,
"Verify database is ready",
maxRetries,
timeBetweenRetries,
func() error {
// Code that might fail and should retry
db := getDatabase(t)
if db == nil {
return fmt.Errorf("database not ready")
}
return nil
},
)
assert.NoError(t, err)
}
func TestWithTimeoutRetry(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Retry with timeout
maxRetries := 30
timeBetweenRetries := time.Second
err := retry.DoWithRetryableErrors(
t,
"Wait for service healthy",
maxRetries,
timeBetweenRetries,
map[string]bool{
"EOF": true,
"connection refused": true,
"temporary failure in name resolution": true,
},
func() error {
return checkServiceHealth(t)
},
)
assert.NoError(t, err)
}
Limpieza y Gestión de Recursos
Properly manage resource cleanup and avoid orphaned resources.
Cleanup patterns:
// test/cleanup_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
)
func TestWithProperCleanup(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
}
// Ensure cleanup runs even if test fails
defer func() {
// Manual cleanup
terraform.Destroy(t, terraformOptions)
// Check for errors
if r := recover(); r != nil {
t.Errorf("Test panicked: %v", r)
}
}()
terraform.InitAndApply(t, terraformOptions)
// Test code...
}
func TestWithSkipDestroy(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../terraform",
SkipDeploy: false,
NoColor: true,
}
// Skip destroy for debugging
if os.Getenv("SKIP_DESTROY") != "" {
defer terraform.Destroy(t, terraformOptions)
}
terraform.InitAndApply(t, terraformOptions)
}
Integración con CI/CD
Run Terratest in CI/CD pipelines.
GitHub Actions:
# .github/workflows/terratest.yml
name: Terratest
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
- run: go mod download
- run: go test -v -timeout 30m ./test/...
Conclusión
Terratest enables automated testing of infrastructure code, catching bugs and configuration errors before production deployment. By writing Go-based infrastructure tests that provision real resources, validate them, and clean up properly, you gain confidence in your infrastructure automation. Combined with CI/CD integration, Terratest creates a safety net ensuring your infrastructure code is reliable, correct, and production-ready.


