Infrastructure as Code (IaC) has revolutionized how we manage and provision infrastructure. Tools like Terraform allow us to define our infrastructure in code, enabling version control, repeatability, and collaboration. However, just like any other code, IaC needs testing. Untested infrastructure code can lead to costly errors, security vulnerabilities, and unexpected downtime.
This blog post introduces the concept of testing your IaC and provides examples of using Terratest to write automated tests for your Terraform modules that provision Google Cloud Platform (GCP) resources.
Why Test Your Infrastructure Code?
Testing your IaC offers numerous benefits:
- Early Error Detection: Identify issues before they impact production environments.
- Increased Confidence: Deploy infrastructure changes with greater assurance.
- Improved Reliability: Ensure your infrastructure behaves as expected.
- Reduced Risk: Minimize the chances of configuration errors and security vulnerabilities.
- Documentation: Tests serve as living documentation of your infrastructure’s intended state.
Introducing Terratest
Terratest is a Go library that makes it easier to write automated tests for your infrastructure code. It provides a variety of helper functions and patterns for testing Terraform, Packer, Docker, Kubernetes, and more. Terratest allows you to write tests that:
- Apply your Terraform code.
- Verify the resources that Terraform created.
- Destroy the resources that Terraform created.
This ensures your infrastructure code not only provisions the correct resources but also cleans them up properly.
Setting Up Your Environment
Before we dive into the examples, let’s set up our environment:
-
Install Go: Terratest is written in Go, so you’ll need to install it. You can download Go from the official website: https://go.dev/dl/
-
Install Terraform: Ensure you have Terraform installed and configured. You can download Terraform from: https://www.terraform.io/downloads
-
Install the gcloud CLI: You’ll need the Google Cloud SDK (gcloud CLI) to interact with your GCP resources. Instructions are available here: https://cloud.google.com/sdk/docs/install
-
Configure gcloud CLI: Authenticate and configure the gcloud CLI to access your GCP project.
gcloud auth login gcloud config set project YOUR_GCP_PROJECT_ID
-
Create a Service Account: Create a service account with necessary permissions to create and manage the GCP resources you’ll be testing. Download the service account key file.
-
Set the GOOGLE_APPLICATION_CREDENTIALS environment variable:
export GOOGLE_APPLICATION_CREDENTIALS="path/to/your/service-account-key.json"
Example: Testing a GCP Bucket
Let’s create a simple Terraform module that provisions a Google Cloud Storage (GCS) bucket and then write a Terratest test to verify it.
1. Terraform Module (main.tf):
# main.tf
resource "google_storage_bucket" "bucket" {
name = "terratest-example-bucket-${random_id.bucket_prefix.hex}"
location = "US"
storage_class = "STANDARD"
force_destroy = true
}
resource "random_id" "bucket_prefix" {
byte_length = 8
}
output "bucket_name" {
value = google_storage_bucket.bucket.name
}
2. Terratest Test (main_test.go):
package test
import (
"fmt"
"strings"
"testing"
"github.com/gruntwork-io/terratest/modules/gcp"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestGCPBucket(t *testing.T) {
t.Parallel()
// 1. Define variables
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("terratest-example-bucket-%s", strings.ToLower(uniqueID)) //Ensure bucket names are lowercase
region := "us-central1" // Change to your desired region
projectID := gcp.GetGoogleProjectIDFromEnvVar(t)
// 2. Terraform options
terraformOptions := &terraform.Options{
TerraformDir: "../examples/gcp-bucket", // Path to your Terraform module
Vars: map[string]interface{}{
"bucket_name": bucketName, // Pass the bucket name as a variable
},
}
// 3. Clean up resources after the test
defer terraform.Destroy(t, terraformOptions)
// 4. Apply the Terraform configuration
terraform.InitAndApply(t, terraformOptions)
// 5. Assertions
outputBucketName := terraform.Output(t, terraformOptions, "bucket_name")
assert.Equal(t, bucketName, outputBucketName, "Bucket name does not match expected value")
// 6. Verify the bucket exists in GCP
gcp.AssertStorageBucketExists(t, bucketName) //Uses default project and region
// 7. Optionally, verify the region
bucket := gcp.GetStorageBucket(t, projectID, bucketName)
assert.Equal(t, region, bucket.Location, "Bucket location does not match expected value")
}
3. Folder Structure:
Organize your project like this:
├── examples
│ └── gcp-bucket
│ └── main.tf
├── test
│ └── main_test.go
└── go.mod
└── go.sum
4. Explanation:
- Import Statements: The
main_test.go
file imports necessary packages, includingterratest/modules/terraform
,terratest/modules/gcp
, andgithub.com/stretchr/testify/assert
for assertions. TestGCPBucket
Function: This is the main test function. Thet.Parallel()
call enables parallel test execution.- Variable Definitions: Defines variables like
bucketName
andregion
that will be used throughout the test. TheuniqueID
ensures that bucket names are unique across test runs. Important to note: bucket names in GCP need to be lowercase. - Terraform Options: The
terraformOptions
struct configures Terraform, specifying the directory containing the Terraform code and any variables to pass to the module. defer terraform.Destroy
: This line schedules the Terraformdestroy
command to run after the test completes, ensuring that the created resources are cleaned up. This is crucial for avoiding unnecessary costs and maintaining a clean environment.terraform.InitAndApply
: This applies the Terraform configuration.- Assertions:
- The test retrieves the
bucket_name
output from Terraform and asserts that it matches the expected value. gcp.AssertStorageBucketExists
verifies that the bucket exists in GCP. This function uses the default project and region configured withgcloud
.gcp.GetStorageBucket
retrieves the bucket metadata from GCP and verifies its location.
- The test retrieves the
- Dependencies: Use
go mod init <your_module_name>
to create ago.mod
file. Then, usego get
to download the necessary dependencies:
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/gcp
go get github.com/stretchr/testify/assert
go get github.com/gruntwork-io/terratest/modules/random
5. Running the Test:
Navigate to the test
directory and run the following command:
go test -v .
Terratest will then initialize, apply, and destroy your infrastructure, providing you with valuable feedback on your configuration.
Best Practices for Terratest
- Keep Tests Isolated: Each test should create its own isolated environment to avoid conflicts. Use unique names for resources.
- Use
defer terraform.Destroy
: Always clean up resources after the test to prevent resource leaks. - Write Comprehensive Tests: Test different scenarios and edge cases.
- Use Helper Functions: Terratest provides many helper functions to simplify your tests. Leverage them!
- Parameterize Tests: Use variables and loops to create more flexible and reusable tests.
Conclusion
Testing your infrastructure code is essential for ensuring the reliability and stability of your deployments. Terratest provides a powerful and flexible framework for writing automated tests for Terraform modules on GCP. By incorporating testing into your IaC workflow, you can catch errors early, reduce risk, and deploy infrastructure changes with confidence. This example provides a basic foundation for testing GCP resources with Terratest. Explore Terratest’s capabilities and adapt the examples to fit your specific infrastructure needs.