End-to-End Testing

End-to-end (E2E) testing for Terraform involves testing complete infrastructure deployments in conditions that closely match production environments. This guide covers modern E2E testing approaches for infrastructure as of 2025.

Overview

E2E tests verify:

  • Complete infrastructure deployment

  • Real-world service interactions

  • Production-like configurations

  • Actual cloud provider behavior

  • Data persistence and recovery

  • Infrastructure scaling

Implementation Strategy

1. Environment Setup

# environments/e2e/main.tf
module "complete_infrastructure" {
  source = "../../"
  
  environment = "e2e"
  region     = var.primary_region
  
  vpc_config = {
    cidr_block = "10.0.0.0/16"
    azs        = ["us-west-2a", "us-west-2b", "us-west-2c"]
  }
  
  database_config = {
    instance_class    = "db.t3.medium"
    engine_version    = "13.7"
    storage_encrypted = true
  }
  
  application_config = {
    instance_type = "t3.medium"
    min_size      = 2
    max_size      = 4
  }
}

2. Test Implementation

package test

import (
    "testing"
    "time"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/stretchr/testify/assert"
)

func TestE2E(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../../environments/e2e",
        Vars: map[string]interface{}{
            "primary_region": "us-west-2",
        },
        EnvVars: map[string]string{
            "AWS_SDK_LOAD_CONFIG": "true",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Test infrastructure deployment
    testInfrastructureDeployment(t, terraformOptions)
    
    // Test scaling and performance
    testAutoScaling(t, terraformOptions)
    
    // Test failure scenarios
    testFailureRecovery(t, terraformOptions)
    
    // Test data persistence
    testDataPersistence(t, terraformOptions)
}

func testInfrastructureDeployment(t *testing.T, options *terraform.Options) {
    // Verify VPC and networking
    vpcId := terraform.Output(t, options, "vpc_id")
    aws.GetVpcById(t, vpcId, options.Vars["primary_region"].(string))
    
    // Verify load balancer
    lbDNS := terraform.Output(t, options, "load_balancer_dns")
    assert.Eventually(t, func() bool {
        return isEndpointHealthy(lbDNS)
    }, 5*time.Minute, 10*time.Second)
    
    // Verify database
    dbEndpoint := terraform.Output(t, options, "database_endpoint")
    assert.Eventually(t, func() bool {
        return isDatabaseConnectable(dbEndpoint)
    }, 5*time.Minute, 10*time.Second)
}

func testAutoScaling(t *testing.T, options *terraform.Options) {
    asgName := terraform.Output(t, options, "autoscaling_group_name")
    
    // Generate load
    generateLoad(t, terraform.Output(t, options, "application_url"))
    
    // Verify scaling
    assert.Eventually(t, func() bool {
        size := getAsgSize(asgName)
        return size > 2 // Min size is 2
    }, 10*time.Minute, 30*time.Second)
}

Infrastructure Testing Patterns

1. Disaster Recovery Testing

func testDisasterRecovery(t *testing.T, options *terraform.Options) {
    // Simulate primary region failure
    primaryRegion := options.Vars["primary_region"].(string)
    secondaryRegion := options.Vars["secondary_region"].(string)
    
    // Force failover
    terraform.Apply(t, options, map[string]interface{}{
        "force_failover": true,
    })
    
    // Verify secondary region
    endpoint := terraform.Output(t, options, "current_endpoint")
    assert.Contains(t, endpoint, secondaryRegion)
    
    // Verify data consistency
    checkDataConsistency(t, endpoint)
    
    // Recover primary
    terraform.Apply(t, options, map[string]interface{}{
        "force_failover": false,
    })
}

2. Security Testing

func testSecurityControls(t *testing.T, options *terraform.Options) {
    // Test IAM roles and permissions
    iamRoles := terraform.OutputList(t, options, "iam_role_arns")
    for _, role := range iamRoles {
        permissions := aws.GetIAMRolePolicy(t, role, options.Vars["primary_region"].(string))
        assert.True(t, validateLeastPrivilege(permissions))
    }
    
    // Test network security
    securityGroups := terraform.OutputList(t, options, "security_group_ids")
    for _, sg := range securityGroups {
        rules := aws.GetSecurityGroupRules(t, sg, options.Vars["primary_region"].(string))
        assert.False(t, hasPublicIngress(rules))
    }
    
    // Test encryption
    validateEncryption(t, options)
}

Performance Testing

1. Load Testing Configuration

func setupLoadTest(t *testing.T) *vegeta.Attacker {
    return vegeta.NewAttacker(
        vegeta.Workers(10),
        vegeta.MaxWorkers(20),
        vegeta.Timeout(30*time.Second),
    )
}

func runLoadTest(t *testing.T, endpoint string) *vegeta.Metrics {
    rate := vegeta.Rate{Freq: 100, Per: time.Second}
    duration := 5 * time.Minute
    
    targeter := vegeta.NewStaticTargeter(vegeta.Target{
        Method: "GET",
        URL:    endpoint,
    })
    
    attacker := setupLoadTest(t)
    var metrics vegeta.Metrics
    
    for res := range attacker.Attack(targeter, rate, duration, "Load Test") {
        metrics.Add(res)
    }
    metrics.Close()
    
    return &metrics
}

2. Scalability Testing

func testScalability(t *testing.T, options *terraform.Options) {
    endpoint := terraform.Output(t, options, "application_url")
    asgName := terraform.Output(t, options, "autoscaling_group_name")
    
    // Baseline metrics
    baselineMetrics := runLoadTest(t, endpoint)
    
    // Scale test
    scaleTestMetrics := make([]*vegeta.Metrics, 3)
    for i := 0; i < 3; i++ {
        // Increase load
        metrics := runLoadTest(t, endpoint)
        scaleTestMetrics[i] = metrics
        
        // Verify scaling behavior
        currentSize := getAsgSize(asgName)
        assert.True(t, currentSize <= 4) // Max size
        
        // Check performance
        assert.Less(t, metrics.Latencies.P95, 500*time.Millisecond)
    }
}

Monitoring and Observability

1. Test Metrics Collection

func collectTestMetrics(t *testing.T, options *terraform.Options) {
    // CloudWatch metrics
    metricCollector := aws.NewCloudWatchMetricCollector(
        options.Vars["primary_region"].(string),
    )
    
    metrics := metricCollector.GetMetrics(map[string]string{
        "Namespace": "AWS/EC2",
        "Period":    "300",
    })
    
    // Log test results
    logger := terratest.NewLogger(t)
    logger.Logf(t, "Test Metrics: %v", metrics)
}

2. Test Result Analysis

func analyzeTestResults(t *testing.T, metrics []*vegeta.Metrics) {
    var summary TestSummary
    
    for _, m := range metrics {
        summary.AddMetrics(m)
    }
    
    // Generate report
    report := summary.GenerateReport()
    
    // Save results
    if err := saveTestResults(report); err != nil {
        t.Errorf("Failed to save test results: %v", err)
    }
}

CI/CD Integration

name: E2E Tests
on:
  schedule:
    - cron: '0 0 * * *'  # Daily
  workflow_dispatch:

jobs:
  e2e-test:
    runs-on: ubuntu-latest
    environment: e2e
    timeout-minutes: 120
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-role
          aws-region: us-west-2
      
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Run E2E Tests
        run: |
          cd test/e2e
          go test -v -timeout 2h ./...
        env:
          TF_VAR_environment: e2e
          TEST_REPORT_PATH: ${{ github.workspace }}/test-results
      
      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: test-results/

Last updated