TL;DR
Google Cloud Platform hosts 9% of global cloud workloads, and infrastructure misconfigurations are responsible for 65% of cloud security incidents, according to the Gartner Cloud Security report 2024. GCP infrastructure testing prevents these incidents by validating Terraform modules, IAM policies, and network configurations before they reach production. Unlike application testing, infrastructure testing requires real cloud resources: a unit test for a GKE cluster must actually create the cluster, verify its configuration, and destroy it — Terratest makes this reproducible in CI/CD pipelines. According to the ThoughtWorks Technology Radar, infrastructure testing with Terratest is now considered a core DevOps practice for organizations operating at scale on Google Cloud. The GCP Policy Library provides 100+ pre-built Rego policies for security and compliance validation, covering everything from storage bucket public access to IAM privilege escalation prevention. This guide covers the complete GCP infrastructure testing stack: Terratest for Terraform module testing, Config Validator for policy enforcement, and gcloud CLI integration for automated compliance reporting.
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
“Infrastructure bugs are 10x more expensive than application bugs because they affect every service running on that infrastructure simultaneously. GCP policy validation catches these issues before deployment — when they cost minutes, not days of incident response.” — Yuri Kan, Senior QA Lead
FAQ
What is GCP infrastructure testing?
Validating Google Cloud resources — Terraform modules, Cloud SQL, GKE clusters, IAM policies — using Terratest for integration tests, Config Validator for policy compliance, and gcloud terraform vet for pre-deployment checks.
What is Terratest and how is it used with GCP?
Go library for infrastructure testing. Spins up real GCP resources, validates their configuration and connectivity, then destroys them. Enables true integration testing of Terraform modules in CI/CD pipelines.
What is GCP Config Validator?
GCP tool that checks Terraform plans against a Policy Library of Rego policies. Validates infrastructure configurations comply with security requirements before deployment — catches privilege escalation and public exposure.
How do you test IAM policies in GCP?
Use gcloud iam test-iam-permissions to verify permissions, Terratest assertions to validate bindings post-deployment, and Config Validator policies to enforce least-privilege across all GCP 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
