Skip to main content

Terraform Ep 15: Infrastructure Unit Testing with Terratest (Golang)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
terraform - This article is part of a series.
Part 15: This Article
If application developers write Unit Tests for their Python and JavaScript code, why do Platform Engineers rarely write tests for their Infrastructure as Code? terraform plan only checks syntax; it does not guarantee that the deployed Load Balancer actually routes traffic correctly. It is time to implement actual testing using Terratest.

1. The Need for Infrastructure Testing
#

Consider the VPC Module we built back in Episode 9. Suppose a junior engineer modifies the module to add an extra subnet. They run terraform plan, and it succeeds. They merge the PR.

However, they accidentally misconfigured the Route Table, meaning the subnet has no internet access. terraform plan cannot catch this error because the AWS API accepts the configuration as valid. You only discover the outage when Production crashes.

Terratest, an open-source Go library developed by Gruntwork, solves this. It allows you to write Go code that:

  1. Programmatically runs terraform init and terraform apply in a temporary test environment.
  2. Uses the AWS SDK (via Go) to reach out to the physical cloud and verify if the resource behaves exactly as expected (e.g., actually making an HTTP GET request to the Load Balancer to ensure it returns a 200 OK).
  3. Always runs terraform destroy at the end, regardless of whether the test passed or failed.

2. Setting Up the Go Environment
#

Because Terratest is a Go library, you must have Golang installed on your machine.

Step 2.1: Install Golang
#

Ensure Go is installed. If you are on Ubuntu:

sudo snap install go --classic
go version
# Expected: go version go1.21.0 linux/amd64

Step 2.2: The Directory Structure
#

Tests in Terraform modules are traditionally placed in a dedicated test/ folder adjacent to your .tf files.

terraform-testing/
├── main.tf
├── outputs.tf
├── variables.tf
└── test/
    ├── go.mod
    ├── go.sum
    └── web_server_test.go    # <--- The Terratest Code

3. Practice: Writing the Terraform Module
#

Let’s write a simple Terraform module that creates an AWS S3 bucket. We will intentionally make it simple so we can focus on the testing mechanics.

Create a new directory and files:

mkdir -p terraform-testing/test
cd terraform-testing

main.tf

variable "bucket_name" {
  type = string
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "test_bucket" {
  bucket = var.bucket_name
}

outputs.tf

output "bucket_arn" {
  value = aws_s3_bucket.test_bucket.arn
}

4. Practice: Writing the Terratest (Golang) Code
#

Now, navigate into the test/ directory to write our Go test.

cd test

Initialize the Go module and download the Terratest dependencies:

go mod init my-terraform-tests
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/aws
go get github.com/stretchr/testify/assert

Create a file named web_server_test.go and paste the following Go code:

package test

import (
	"fmt"
	"strings"
	"testing"
	"time"

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

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

	// 1. Generate a random, unique name for the bucket to prevent test collisions
	expectedBucketName := fmt.Sprintf("terratest-demo-bucket-%d", time.Now().Unix())

	// 2. Define the Terraform Options
	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		// The path to where our Terraform code is located
		TerraformDir: "../",

		// Variables to pass to our Terraform code using -var options
		Vars: map[string]interface{}{
			"bucket_name": expectedBucketName,
		},
	})

	// 3. Clean up the infrastructure at the end of the test.
	// defer ensures this runs even if the assertions below fail!
	defer terraform.Destroy(t, terraformOptions)

	// 4. Run `terraform init` and `terraform apply`
	terraform.InitAndApply(t, terraformOptions)

	// 5. Run `terraform output` to get the value of an output variable
	bucketARN := terraform.Output(t, terraformOptions, "bucket_arn")

	// 6. Assertions!
	// We use the AWS SDK (via Terratest) to actually look inside the AWS account
	// and verify the bucket exists and matches our expectations.
	aws.AssertS3BucketExists(t, "us-east-1", expectedBucketName)
	
	// We verify the output ARN matches standard AWS format
	assert.True(t, strings.HasPrefix(bucketARN, "arn:aws:s3:::"))
	assert.True(t, strings.HasSuffix(bucketARN, expectedBucketName))
}

5. Execution and Observation
#

To run the test, ensure your AWS CLI credentials are fully configured (as learned in Episode 2), and execute the standard Go test command from inside the test/ directory.

go test -v -timeout 30m

Expected Terminal Output:

=== RUN   TestTerraformS3Bucket
=== PAUSE TestTerraformS3Bucket
=== CONT  TestTerraformS3Bucket
TestTerraformS3Bucket 2023-10-24T10:00:00Z retry.go:91: terraform [init -upgrade=false]
TestTerraformS3Bucket 2023-10-24T10:00:00Z logger.go:66: Running command terraform with args [init -upgrade=false]
...
TestTerraformS3Bucket 2023-10-24T10:00:03Z retry.go:91: terraform [apply -auto-approve -var bucket_name=terratest-demo-bucket-1698141600]
...
TestTerraformS3Bucket 2023-10-24T10:00:05Z logger.go:66: aws_s3_bucket.test_bucket: Creation complete after 1s [id=terratest-demo-bucket-1698141600]
TestTerraformS3Bucket 2023-10-24T10:00:05Z logger.go:66: Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
...
TestTerraformS3Bucket 2023-10-24T10:00:05Z retry.go:91: terraform [output -no-color -json bucket_arn]
TestTerraformS3Bucket 2023-10-24T10:00:06Z logger.go:66: Asserting S3 bucket terratest-demo-bucket-1698141600 exists in us-east-1
...
TestTerraformS3Bucket 2023-10-24T10:00:07Z retry.go:91: terraform [destroy -auto-approve -var bucket_name=terratest-demo-bucket-1698141600]
...
TestTerraformS3Bucket 2023-10-24T10:00:10Z logger.go:66: Destroy complete! Resources: 1 destroyed.
--- PASS: TestTerraformS3Bucket (10.45s)
PASS
ok      my-terraform-tests      10.460s

Look at that beautiful timeline! The Go code initialized Terraform, created the bucket, asserted its existence in the AWS API, and successfully destroyed it all in exactly 10.46 seconds.

If you integrate this go test command into the GitHub Actions pipeline we built in Episode 12, your Terraform Modules will never break Production again.


Troubleshooting & Common Errors
#

  1. go: go.mod file not found in current directory or any parent directory

    • Root Cause: You forgot to initialize the Go module.
    • Solution: Run go mod init <module-name> inside your test/ directory before running go get or go test.
  2. Test timed out or Orphaned Resources

    • Root Cause: If your Go test crashes (e.g., due to a syntax error or forced termination Ctrl+C) before the defer terraform.Destroy() command executes, the infrastructure will remain running in AWS forever, costing you money.
    • Solution: Gruntwork provides an advanced tool called cloud-nuke specifically designed to sweep your AWS accounts and destroy any orphaned resources left behind by failed Terratests.

Curriculum Conclusion 🎓
#

You have reached the absolute pinnacle of this curriculum.

Over the past 15 episodes, you have evolved from manually clicking through the AWS Console, to declaring basic resources, building dynamic modules and loops, scaling via terragrunt, and finally reaching the holy grail of Platform Engineering: Automated Infrastructure Unit Testing.

You are no longer an operator; you are a Senior Platform Engineer.

Thank you for embarking on this incredible Zero to Hero journey. Your cloud infrastructure will never be the same again.

terraform - This article is part of a series.
Part 15: This Article