TL;DR
- Use Terratest for integration testing GCP Terraform modules—deploy real resources, verify with GCP APIs, then destroy
- Combine
gcloud terraform vetwith Policy Library constraints for pre-deployment policy validation- Native Terraform testing (v1.6+) handles unit tests; Terratest handles integration tests that need real GCP resources
Best for: Teams managing GCP infrastructure with Terraform who need compliance validation and deployment confidence Skip if: You’re using GCP Console exclusively or have fewer than 5 Terraform modules Read time: 12 minutes
Testing GCP infrastructure differs from AWS or Azure testing due to Google’s unique resource model, IAM structure, and the Config Validator ecosystem. This guide covers practical testing approaches that leverage GCP-native tools alongside universal infrastructure testing patterns.
Understanding the Terraform testing pyramid provides context for where GCP-specific testing fits within your overall IaC validation strategy.
AI-Assisted Approaches
AI tools accelerate GCP infrastructure test development, especially for generating constraint templates and Terratest assertions.
Generating Terratest assertions for GCP resources:
I have a Terraform module that creates:
- GCP Compute Instance with custom metadata
- VPC network with firewall rules
- Cloud Storage bucket with lifecycle policies
Generate Terratest assertions in Go that verify:
1. Instance is running and has correct machine type
2. Firewall rules allow only ports 443 and 22
3. Bucket has versioning enabled and lifecycle rule for 30-day deletion
4. All resources have required labels: environment, team, cost-center
Include proper error handling and use google.golang.org/api packages.
Creating Config Validator constraints:
Write a Rego constraint template for Config Validator that enforces:
1. All GCS buckets must have uniform bucket-level access enabled
2. All Compute instances must not have external IP addresses
3. All VPCs must have flow logs enabled
Use the GCP Policy Library format with proper CAI asset types
and include sample constraint YAML files.
Debugging failed terraform vet validations:
My terraform vet command fails with this output:
[paste vet output]
The constraint is:
[paste constraint YAML]
My Terraform plan includes:
[paste relevant terraform plan output]
Explain why the validation fails and how to fix either
the Terraform code or the constraint.
When to Use Each Testing Approach
Testing Strategy Decision Framework
| Test Type | Tool | When to Use | GCP Resources Required |
|---|---|---|---|
| Syntax validation | terraform validate | Every commit | None |
| Static analysis | tflint-ruleset-google | Every commit | None |
| Policy pre-check | gcloud terraform vet | Before plan/apply | API access only |
| Unit testing | Terraform test (native) | Module logic | None (mocked) |
| Integration testing | Terratest | Real resource validation | Full project access |
| End-to-end | Terratest + application tests | Production-like environments | Dedicated test project |
Use Terratest When
- You need to verify actual GCP resource properties: Metadata, labels, network configs
- Testing cross-resource interactions: IAM bindings, firewall rules affecting instances
- Validating GCP-specific behaviors: Managed instance group scaling, Cloud SQL failover
- CI/CD requires deployment verification: Resources must exist before proceeding
Use Config Validator/terraform vet When
- Enforcing organizational policies: No public IPs, encryption required, specific regions
- Pre-deployment compliance checks: Block non-compliant plans before apply
- Standardizing across teams: Consistent tagging, naming conventions, resource configurations
Terratest for GCP Modules
Basic Module Test Structure
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/gcp"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestGCPComputeInstance(t *testing.T) {
t.Parallel()
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
region := "us-central1"
zone := "us-central1-a"
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/compute",
Vars: map[string]interface{}{
"project_id": projectID,
"region": region,
"zone": zone,
"instance_name": "test-instance-" + random.UniqueId(),
"machine_type": "e2-micro",
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Verify instance properties
instanceName := terraform.Output(t, terraformOptions, "instance_name")
instance := gcp.FetchInstance(t, projectID, zone, instanceName)
assert.Equal(t, "RUNNING", instance.Status)
assert.Contains(t, instance.MachineType, "e2-micro")
}
Testing VPC and Firewall Rules
func TestGCPNetworkWithFirewall(t *testing.T) {
t.Parallel()
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/network",
Vars: map[string]interface{}{
"project_id": projectID,
"network_name": "test-vpc-" + random.UniqueId(),
"allowed_ports": []string{"443", "22"},
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
networkName := terraform.Output(t, terraformOptions, "network_name")
// Verify firewall rules
firewalls := gcp.GetFirewallRulesForNetwork(t, projectID, networkName)
for _, fw := range firewalls {
// Ensure no rule allows 0.0.0.0/0 on sensitive ports
for _, allowed := range fw.Allowed {
if contains(fw.SourceRanges, "0.0.0.0/0") {
assert.NotContains(t, allowed.Ports, "3389",
"RDP should not be open to internet")
assert.NotContains(t, allowed.Ports, "3306",
"MySQL should not be open to internet")
}
}
}
}
Testing Cloud Storage with Lifecycle Rules
func TestGCSBucketCompliance(t *testing.T) {
t.Parallel()
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
bucketName := "test-bucket-" + strings.ToLower(random.UniqueId())
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/storage",
Vars: map[string]interface{}{
"project_id": projectID,
"bucket_name": bucketName,
"location": "US",
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Use GCP client library for detailed bucket inspection
ctx := context.Background()
client, err := storage.NewClient(ctx)
require.NoError(t, err)
defer client.Close()
bucket := client.Bucket(bucketName)
attrs, err := bucket.Attrs(ctx)
require.NoError(t, err)
// Verify compliance requirements
assert.True(t, attrs.UniformBucketLevelAccess.Enabled,
"Uniform bucket-level access must be enabled")
assert.True(t, attrs.VersioningEnabled,
"Versioning must be enabled")
assert.NotEmpty(t, attrs.Lifecycle.Rules,
"Lifecycle rules must be configured")
}
Config Validator and Policy Library
Setting Up Policy Library
# Clone the GCP policy library
git clone https://github.com/GoogleCloudPlatform/policy-library.git
cd policy-library
# Structure
# policies/
# constraints/ # Your constraint definitions
# templates/ # Constraint templates (Rego rules)
Creating Custom Constraints
# policies/constraints/require_bucket_versioning.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: GCPStorageBucketVersioningConstraintV1
metadata:
name: require-bucket-versioning
spec:
severity: high
match:
ancestries:
- "organizations/123456789"
excludedAncestries: []
parameters:
versioning_enabled: true
Constraint Template Example
# policies/templates/gcp_storage_bucket_versioning.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: gcpstoragebucketversioningconstraintv1
spec:
crd:
spec:
names:
kind: GCPStorageBucketVersioningConstraintV1
validation:
openAPIV3Schema:
properties:
versioning_enabled:
type: boolean
targets:
- target: validation.gcp.forsetisecurity.org
rego: |
package templates.gcp.GCPStorageBucketVersioningConstraintV1
violation[{
"msg": message,
"details": metadata,
}] {
asset := input.asset
asset.asset_type == "storage.googleapis.com/Bucket"
versioning := asset.resource.data.versioning
not versioning.enabled
message := sprintf("Bucket %v does not have versioning enabled", [asset.name])
metadata := {"resource": asset.name}
}
Using gcloud terraform vet
# Generate Terraform plan in JSON format
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# Validate against policy library
gcloud beta terraform vet tfplan.json \
--policy-library=./policy-library \
--project=my-project
# In CI/CD pipeline
gcloud beta terraform vet tfplan.json \
--policy-library=./policy-library \
--format=json > violations.json
# Check for violations
if [ $(jq '.violations | length' violations.json) -gt 0 ]; then
echo "Policy violations found:"
jq '.violations[] | .message' violations.json
exit 1
fi
CI/CD Integration
GitHub Actions Workflow
name: GCP Infrastructure Tests
on:
pull_request:
paths:
- 'terraform/**'
- 'policies/**'
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Validate
run: terraform validate
working-directory: terraform/
- name: TFLint with GCP ruleset
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: latest
- run: |
tflint --init
tflint --format=compact
working-directory: terraform/
policy-validation:
runs-on: ubuntu-latest
needs: static-analysis
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@v2
- name: Generate plan
run: |
terraform init
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
working-directory: terraform/
- name: Policy validation
run: |
gcloud beta terraform vet tfplan.json \
--policy-library=../policies \
--format=json > violations.json
if [ $(jq '.violations | length' violations.json) -gt 0 ]; then
echo "::error::Policy violations detected"
jq -r '.violations[] | "- \(.constraint): \(.message)"' violations.json
exit 1
fi
working-directory: terraform/
integration-tests:
runs-on: ubuntu-latest
needs: policy-validation
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Run Terratest
run: |
cd test
go test -v -timeout 30m -parallel 4
env:
GOOGLE_PROJECT: ${{ secrets.GCP_TEST_PROJECT }}
Common GCP Testing Patterns
Testing IAM Bindings
func TestIAMBindings(t *testing.T) {
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
// After terraform apply...
// Verify service account has expected roles
saEmail := terraform.Output(t, terraformOptions, "service_account_email")
policy, err := getProjectIAMPolicy(projectID)
require.NoError(t, err)
expectedRole := "roles/storage.objectViewer"
member := "serviceAccount:" + saEmail
found := false
for _, binding := range policy.Bindings {
if binding.Role == expectedRole {
for _, m := range binding.Members {
if m == member {
found = true
break
}
}
}
}
assert.True(t, found, "Service account should have %s role", expectedRole)
}
Testing Private Google Access
func TestPrivateGoogleAccess(t *testing.T) {
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
// After creating subnet...
subnetName := terraform.Output(t, terraformOptions, "subnet_name")
region := "us-central1"
subnet := gcp.FetchSubnet(t, projectID, region, subnetName)
assert.True(t, subnet.PrivateIpGoogleAccess,
"Private Google Access should be enabled for private subnets")
}
Measuring Success
| Metric | Before Testing | After Testing | How to Track |
|---|---|---|---|
| Policy violations in prod | Unknown | 0 | Config Validator reports |
| Failed deployments | 15%/month | <2%/month | CI/CD pipeline metrics |
| Compliance audit findings | 5-10/quarter | 0-1/quarter | Audit reports |
| Mean time to detect issues | Days | Minutes | Alert timestamps |
Warning signs your testing isn’t working:
- Policy violations still reaching production
- Terratest passing but real issues in deployed resources
- Tests taking >30 minutes (resource leaks likely)
- Flaky tests due to eventual consistency
Conclusion
GCP infrastructure testing combines universal IaC testing practices with Google-specific tools:
- Use Terratest for integration tests requiring real GCP resources
- Implement Config Validator constraints for organizational policies
- Run gcloud terraform vet in CI/CD to catch violations early
- Leverage the Policy Library for common compliance patterns
The key is layering these tools—static analysis catches syntax issues, policy validation catches compliance issues, and integration tests catch functional issues.
Official Resources
See Also
- Terraform Testing and Validation Strategies - Complete Terraform testing pyramid
- AWS Infrastructure Testing with LocalStack - Compare AWS testing approaches
- Kubernetes Testing Strategies - GKE and container testing
- Policy as Code Testing with OPA and Sentinel - Deep dive into policy languages
- Infrastructure as Code Testing - Foundational IaC testing concepts
