Terraform
TerraformIntermediate

Infrastructure Testing with Terratest - Ensuring Reliability

DevHub Team
4 min read
IaCDevOpsTestingGo

TL;DR

Learn how to test your infrastructure code using Terratest to ensure reliability and prevent issues

Infrastructure Testing with Terratest

Learn how to effectively test your infrastructure code using Terratest to ensure reliability and catch issues before they reach production.

$1

1. Catch configuration errors early

2. Validate infrastructure behavior

3. Ensure compliance and security

4. Test infrastructure changes safely

5. Automate validation processes

$1

$1

``bash

Install Go

brew install go

Install Terraform

brew install terraform

Set up Go workspace

mkdir -p ~/go/src/terraform-aws-example

cd ~/go/src/terraform-aws-example

`

$1

`

terraform-aws-example/

├── main.tf

├── variables.tf

├── outputs.tf

├── test/

│ └── main_test.go

└── go.mod

`

$1

`go

// test/main_test.go

package test

import (

"testing"

"github.com/gruntwork-io/terratest/modules/terraform"

"github.com/stretchr/testify/assert"

)

func TestTerraformBasicExample(t *testing.T) {

terraformOptions := &terraform.Options{

TerraformDir: "../",

Vars: map[string]interface{}{

"region": "us-west-2",

"instance_type": "t2.micro",

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

}

`

$1

$1

`go

func TestEC2Instance(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/ec2",

Vars: map[string]interface{}{

"instance_type": "t2.micro",

"ami_id": "ami-0c55b159cbfafe1f0",

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

instanceID := terraform.Output(t, terraformOptions, "instance_id")

aws.WaitForInstanceRunning(t, instanceID, "us-west-2")

instanceType := aws.GetInstanceType(t, instanceID, "us-west-2")

assert.Equal(t, "t2.micro", instanceType)

}

`

$1

`go

func TestVPCConfiguration(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/vpc",

Vars: map[string]interface{}{

"vpc_cidr": "10.0.0.0/16",

"environment": "test",

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

vpcID := terraform.Output(t, terraformOptions, "vpc_id")

cidrBlock := aws.GetVpcCidrBlock(t, vpcID, "us-west-2")

assert.Equal(t, "10.0.0.0/16", cidrBlock)

}

`

$1

`go

func TestS3Bucket(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/s3",

Vars: map[string]interface{}{

"bucket_name": "test-bucket",

"versioning_enabled": true,

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

bucketName := terraform.Output(t, terraformOptions, "bucket_name")

versioning := aws.GetS3BucketVersioning(t, "us-west-2", bucketName)

assert.Equal(t, "Enabled", versioning)

}

`

$1

$1

`go

func TestWebServer(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/web-server",

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

url := terraform.Output(t, terraformOptions, "url")

http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello, World!", 30, 5*time.Second)

}

`

$1

`go

func TestRDSInstance(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/rds",

Vars: map[string]interface{}{

"db_name": "testdb",

"db_user": "admin",

"db_password": "password123",

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

dbEndpoint := terraform.Output(t, terraformOptions, "db_endpoint")

dbPort := terraform.Output(t, terraformOptions, "db_port")

// Test database connection

connectionString := fmt.Sprintf(

"host=%s port=%s user=%s password=%s dbname=%s",

dbEndpoint, dbPort, "admin", "password123", "testdb",

)

db, err := sql.Open("postgres", connectionString)

assert.NoError(t, err)

defer db.Close()

err = db.Ping()

assert.NoError(t, err)

}

`

$1

`go

func TestLoadBalancer(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/alb",

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

albDNS := terraform.Output(t, terraformOptions, "alb_dns_name")

// Test load balancer health

http_helper.HttpGetWithRetryWithCustomValidation(

t,

fmt.Sprintf("http://%s", albDNS),

nil,

30,

5*time.Second,

func(status int, body string) bool {

return status == 200

},

)

}

`

$1

$1

`go

func TestInParallel(t *testing.T) {

t.Parallel()

testCases := []struct {

name string

region string

environment string

}{

{"DevEnvironment", "us-west-2", "dev"},

{"StagingEnvironment", "us-east-1", "staging"},

{"ProdEnvironment", "eu-west-1", "prod"},

}

for _, tc := range testCases {

tc := tc // capture range variable

t.Run(tc.name, func(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../",

Vars: map[string]interface{}{

"region": tc.region,

"environment": tc.environment,

},

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

})

}

}

`

$1

`go

func TestWithRetry(t *testing.T) {

t.Parallel()

terraformOptions := &terraform.Options{

TerraformDir: "../examples/ec2",

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

instanceID := terraform.Output(t, terraformOptions, "instance_id")

retry.DoWithRetry(t, "Check instance tags", 30, 5*time.Second, func() (string, error) {

tags := aws.GetTagsForEc2Instance(t, instanceID, "us-west-2")

if val, ok := tags["Environment"]; ok {

return val, nil

}

return "", fmt.Errorf("Environment tag not found")

})

}

`

$1

`go

func TestWithCleanup(t *testing.T) {

t.Parallel()

tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/cleanup-test")

defer test_structure.CleanupTestDataFolder(t, tempTestFolder)

terraformOptions := &terraform.Options{

TerraformDir: tempTestFolder,

}

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

}

`

$1

$1

`yaml

name: Terratest

on: [push, pull_request]

jobs:

test:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v3

- name: Set up Go

uses: actions/setup-go@v3

with:

go-version: '1.19'

- name: Install Terraform

uses: hashicorp/setup-terraform@v2

- name: Run Terratest

run: |

cd test

go test -v ./...

env:

AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

`

$1

$1

`bash

Generate test coverage report

go test -coverprofile=coverage.out ./...

go tool cover -html=coverage.out -o coverage.html

`

$1

`go

func TestWithLogs(t *testing.T) {

logger.Log(t, "Starting test...")

terraformOptions := &terraform.Options{

TerraformDir: "../",

Logger: logger.TestingT,

}

logger.Log(t, "Applying Terraform configuration...")

terraform.InitAndApply(t, terraformOptions)

logger.Log(t, "Test completed successfully")

}

``

$1

Effective infrastructure testing:

  • Ensures reliability
  • Prevents costly mistakes
  • Enables confident changes
  • Improves code quality
  • Facilitates automation
  • Remember to:

    1. Write comprehensive tests

    2. Use proper cleanup

    3. Implement retry logic

    4. Test in parallel when possible

    5. Integrate with CI/CD pipelines

    Why This Matters

    Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

    Trade-offs to Consider

    Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

    When NOT to Use This

    Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

    Decision Framework

    Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.