TL;DR

  • Multi-cloud testing requires provider-agnostic assertions and cloud-specific setup/teardown
  • Use Terratest with multiple provider configurations, not separate test suites per cloud
  • The #1 mistake: testing clouds in isolation instead of testing cross-cloud interactions

Best for: Organizations with workloads distributed across AWS, Azure, and/or GCP Skip if: You’re committed to a single cloud provider with no migration plans Read time: 11 minutes

Your company runs Kubernetes on GKE, databases on AWS RDS, and identity on Azure AD. A Terraform change needs to work across all three. How do you test that the networking allows cross-cloud communication? That DNS resolves correctly? That IAM permissions work end-to-end?

Multi-cloud infrastructure testing is hard because each provider has different APIs, different resource models, and different failure modes. But in 2026, multi-cloud isn’t optional for many organizations — it’s reality. Your testing strategy needs to match.

The Real Problem

Single-cloud testing is straightforward: spin up resources, validate, tear down. Multi-cloud introduces complexity:

Different authentication models: AWS uses IAM roles, Azure uses service principals, GCP uses service accounts. Your test runner needs credentials for all.

Different resource lifecycles: An Azure resource might take 5 minutes to provision while the equivalent AWS resource takes 30 seconds. Timeouts and retries need cloud-specific tuning.

Cross-cloud dependencies: Your app in GCP needs to reach a database in AWS. Testing this requires both clouds running simultaneously, with networking configured.

Inconsistent APIs: Each cloud’s SDK behaves differently. Error handling, pagination, and eventual consistency all vary.

Testing Architecture

The key insight: test modules should be cloud-agnostic where possible, with cloud-specific implementations plugged in.

tests/
├── integration/
│   ├── network_test.go          # Cloud-agnostic network assertions
│   ├── database_test.go         # Cloud-agnostic database assertions
│   └── identity_test.go         # Cloud-agnostic identity assertions
├── providers/
│   ├── aws/
│   │   └── setup.go             # AWS-specific test setup
│   ├── azure/
│   │   └── setup.go             # Azure-specific test setup
│   └── gcp/
│       └── setup.go             # GCP-specific test setup
└── fixtures/
    ├── aws/
    ├── azure/
    └── gcp/

Terratest Multi-Provider Pattern

Terratest supports testing across clouds. The pattern: initialize multiple providers, deploy to each, then run cross-cloud assertions.

package test

import (
    "testing"
    "time"

    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/azure"
    "github.com/gruntwork-io/terratest/modules/gcp"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

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

    // Deploy AWS infrastructure
    awsOpts := &terraform.Options{
        TerraformDir: "../fixtures/aws/networking",
        Vars: map[string]interface{}{
            "environment": "test",
            "vpc_cidr":    "10.0.0.0/16",
        },
        EnvVars: map[string]string{
            "AWS_DEFAULT_REGION": "us-east-1",
        },
    }
    defer terraform.Destroy(t, awsOpts)
    terraform.InitAndApply(t, awsOpts)

    // Deploy GCP infrastructure
    gcpOpts := &terraform.Options{
        TerraformDir: "../fixtures/gcp/networking",
        Vars: map[string]interface{}{
            "environment": "test",
            "vpc_cidr":    "10.1.0.0/16",
        },
        EnvVars: map[string]string{
            "GOOGLE_PROJECT": "my-project",
            "GOOGLE_REGION":  "us-central1",
        },
    }
    defer terraform.Destroy(t, gcpOpts)
    terraform.InitAndApply(t, gcpOpts)

    // Get outputs for cross-cloud validation
    awsVpcId := terraform.Output(t, awsOpts, "vpc_id")
    gcpNetworkName := terraform.Output(t, gcpOpts, "network_name")

    // Validate AWS side
    vpc := aws.GetVpcById(t, awsVpcId, "us-east-1")
    assert.Equal(t, "10.0.0.0/16", vpc.CidrBlock)

    // Validate GCP side
    network := gcp.GetNetwork(t, "my-project", gcpNetworkName)
    assert.True(t, network.AutoCreateSubnetworks == false)

    // Test cross-cloud connectivity (via VPN/interconnect)
    testCrossCloudConnectivity(t, awsOpts, gcpOpts)
}

func testCrossCloudConnectivity(t *testing.T, awsOpts, gcpOpts *terraform.Options) {
    awsInstanceIP := terraform.Output(t, awsOpts, "test_instance_private_ip")
    gcpInstanceIP := terraform.Output(t, gcpOpts, "test_instance_private_ip")

    // SSH to AWS instance and ping GCP instance
    aws.CheckSshCommand(t,
        terraform.Output(t, awsOpts, "test_instance_public_ip"),
        "ubuntu",
        fmt.Sprintf("ping -c 3 %s", gcpInstanceIP),
    )
}

Handling Provider Authentication

Multi-cloud tests need credentials for each provider. Use environment variables with prefixes:

# AWS credentials
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="us-east-1"

# Azure credentials
export ARM_CLIENT_ID="..."
export ARM_CLIENT_SECRET="..."
export ARM_SUBSCRIPTION_ID="..."
export ARM_TENANT_ID="..."

# GCP credentials
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
export GOOGLE_PROJECT="my-project"

For CI/CD, use OIDC where possible:

# GitHub Actions with multi-cloud OIDC
jobs:
  multi-cloud-test:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions
          aws-region: us-east-1

      - name: Configure Azure credentials
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Configure GCP credentials
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github-actions
          service_account: github-actions@my-project.iam.gserviceaccount.com

      - name: Run multi-cloud tests
        run: go test -v ./tests/integration/...

Cloud-Agnostic Test Assertions

Write assertions that work regardless of provider:

package assertions

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

// NetworkConfig represents cloud-agnostic network properties
type NetworkConfig struct {
    CIDR          string
    SubnetCount   int
    HasNATGateway bool
    HasVPNGateway bool
}

// DatabaseConfig represents cloud-agnostic database properties
type DatabaseConfig struct {
    Engine            string // "postgres", "mysql"
    Version           string
    MultiAZ           bool
    EncryptedAtRest   bool
    BackupRetention   int
}

// AssertNetworkConfig validates network regardless of cloud
func AssertNetworkConfig(t *testing.T, expected, actual NetworkConfig) {
    assert.Equal(t, expected.CIDR, actual.CIDR, "CIDR mismatch")
    assert.Equal(t, expected.SubnetCount, actual.SubnetCount, "Subnet count mismatch")
    assert.Equal(t, expected.HasNATGateway, actual.HasNATGateway, "NAT gateway mismatch")
}

// AssertDatabaseConfig validates database regardless of cloud
func AssertDatabaseConfig(t *testing.T, expected, actual DatabaseConfig) {
    assert.Equal(t, expected.Engine, actual.Engine)
    assert.True(t, actual.EncryptedAtRest, "Database must be encrypted at rest")
    assert.GreaterOrEqual(t, actual.BackupRetention, 7, "Backup retention must be >= 7 days")
}

Then implement cloud-specific adapters:

// aws/adapter.go
func GetNetworkConfig(t *testing.T, vpcId, region string) NetworkConfig {
    vpc := aws.GetVpcById(t, vpcId, region)
    subnets := aws.GetSubnetsForVpc(t, vpcId, region)
    natGateways := aws.GetNatGatewaysForVpc(t, vpcId, region)

    return NetworkConfig{
        CIDR:          vpc.CidrBlock,
        SubnetCount:   len(subnets),
        HasNATGateway: len(natGateways) > 0,
    }
}

// gcp/adapter.go
func GetNetworkConfig(t *testing.T, projectId, networkName string) NetworkConfig {
    network := gcp.GetNetwork(t, projectId, networkName)
    subnets := gcp.GetSubnetsForNetwork(t, projectId, networkName)
    routers := gcp.GetRoutersForNetwork(t, projectId, networkName)

    hasNAT := false
    for _, router := range routers {
        if len(router.Nats) > 0 {
            hasNAT = true
            break
        }
    }

    return NetworkConfig{
        CIDR:          subnets[0].IpCidrRange, // GCP uses subnet CIDR
        SubnetCount:   len(subnets),
        HasNATGateway: hasNAT,
    }
}

Testing Cross-Cloud Services

Some tests must validate actual cross-cloud communication:

func TestCrossCloudDatabaseAccess(t *testing.T) {
    // Deploy AWS RDS
    awsOpts := terraform.Options{
        TerraformDir: "../fixtures/aws/rds",
    }
    defer terraform.Destroy(t, &awsOpts)
    terraform.InitAndApply(t, &awsOpts)

    // Deploy GCP VM that connects to AWS RDS
    gcpOpts := terraform.Options{
        TerraformDir: "../fixtures/gcp/client-vm",
        Vars: map[string]interface{}{
            "database_host": terraform.Output(t, &awsOpts, "rds_endpoint"),
        },
    }
    defer terraform.Destroy(t, &gcpOpts)
    terraform.InitAndApply(t, &gcpOpts)

    // Run SQL query from GCP VM to AWS RDS
    vmIP := terraform.Output(t, &gcpOpts, "instance_ip")
    result := gcp.RunCommandOnInstance(t, vmIP, "psql -h $DB_HOST -c 'SELECT 1'")
    assert.Contains(t, result, "1 row")
}

Parallel vs Sequential Testing

Multi-cloud tests can run in parallel across clouds but may need sequential execution within a cloud:

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

    // These can run simultaneously
    t.Run("AWS", func(t *testing.T) {
        t.Parallel()
        testAWSInfrastructure(t)
    })

    t.Run("Azure", func(t *testing.T) {
        t.Parallel()
        testAzureInfrastructure(t)
    })

    t.Run("GCP", func(t *testing.T) {
        t.Parallel()
        testGCPInfrastructure(t)
    })
}

func TestCrossCloudDependent(t *testing.T) {
    // These must run sequentially - GCP depends on AWS
    awsResult := testAWSNetworking(t)
    testGCPWithAWSConnection(t, awsResult)
}

AI-Assisted Approaches

Multi-cloud testing involves understanding multiple provider APIs and their differences. AI tools accelerate this.

What AI does well:

  • Translating test logic from one cloud’s SDK to another
  • Identifying equivalent resources across clouds (RDS vs Cloud SQL vs Azure Database)
  • Generating cloud-agnostic assertion frameworks
  • Explaining differences in resource behavior across providers

What still needs humans:

  • Designing test architecture for cross-cloud dependencies
  • Deciding which properties truly need cross-cloud validation
  • Understanding business requirements driving multi-cloud
  • Debugging cloud-specific authentication issues

Useful prompt:

I have a Terratest that validates AWS VPC with these properties:
- 3 subnets across AZs
- NAT gateway in each AZ
- VPC flow logs enabled

Generate equivalent test code for:
1. GCP VPC Network
2. Azure Virtual Network
Include cloud-agnostic assertions that work for all three.

When This Breaks Down

Multi-cloud testing has limitations:

Cost multiplication: Running tests in three clouds costs 3x. Ephemeral environments help but don’t eliminate the cost.

Credential complexity: Managing OIDC or service accounts for multiple clouds in CI is non-trivial. Rotation, least-privilege, and audit trails multiply.

Timing inconsistencies: Azure might take 10 minutes for a resource that AWS provisions in 1 minute. Tests need cloud-specific timeouts.

Feature parity gaps: Not every cloud has equivalent services. Testing “database” works, but testing “Aurora Serverless” has no Azure equivalent.

Consider focusing:

  • Test cross-cloud interactions thoroughly (these are unique risks)
  • Test cloud-specific features in single-cloud test suites
  • Use contract tests for cloud-agnostic assertions

Decision Framework

Use unified multi-cloud tests when:

  • Workloads actually span multiple clouds
  • Cross-cloud networking is configured (VPN, interconnect)
  • Single team owns infrastructure across clouds

Use separate per-cloud tests when:

  • Each cloud serves different workloads (no cross-cloud dependencies)
  • Different teams own different clouds
  • Cloud-specific features dominate over common patterns

Use abstraction layers when:

  • Planning for cloud migration or multi-cloud future
  • Want portable infrastructure (cloud-agnostic modules)
  • Cost optimization drives cloud selection dynamically

Measuring Success

MetricBeforeAfterHow to Track
Cross-cloud incidentsUnknown0Incident reports
Test coverage per cloudVariable80%+ eachCoverage reports
Mean test durationN/A<15 minCI metrics
Cloud parity verificationManualAutomatedTest assertions

Warning signs it’s not working:

  • Tests passing but cross-cloud issues in production
  • Skipping clouds in CI due to credential issues
  • Massive .gitignore for cloud-specific test outputs
  • Different teams writing duplicate tests per cloud

What’s Next

Start with your actual cross-cloud touchpoints:

  1. Map which resources in cloud A depend on resources in cloud B
  2. Write tests for those specific interactions first
  3. Build cloud-agnostic assertion library incrementally
  4. Expand to full infrastructure coverage per cloud
  5. Run cross-cloud tests on every PR affecting shared modules

The goal isn’t testing every cloud equally — it’s testing the seams where clouds connect.


Related articles:

External resources: