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
| Metric | Before | After | How to Track |
|---|---|---|---|
| Cross-cloud incidents | Unknown | 0 | Incident reports |
| Test coverage per cloud | Variable | 80%+ each | Coverage reports |
| Mean test duration | N/A | <15 min | CI metrics |
| Cloud parity verification | Manual | Automated | Test 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:
- Map which resources in cloud A depend on resources in cloud B
- Write tests for those specific interactions first
- Build cloud-agnostic assertion library incrementally
- Expand to full infrastructure coverage per cloud
- 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:
- AWS Infrastructure Testing with LocalStack
- Azure Infrastructure Testing
- Terratest for Infrastructure as Code
- Infrastructure as Code Testing
External resources: