Testing AWS resources with Terratest

This guide will walk you through adding Terratest to a Nitric project.

How Terratest works

Terratest is designed to automate the entire process of testing your Terraform code with the following steps:

  • Initialize: Terratest will automatically run terraform init to initialize the Terraform working directory.
  • Apply: It will then run terraform apply to deploy the infrastructure as defined in your Terraform code.
  • Assert: The test script will then run assertions to check that the infrastructure was created as expected.
  • Teardown: Finally, it will run terraform destroy to tear down the infrastructure after the test completes.

What we'll be doing

  1. Create and set up your application.
  2. Deploying to AWS with a Terraform provider.
  3. Add and execute Terratest

Create and set up your application.

Our sample project creates a real-time communication service using websockets and a key-value store for connections.

We intend to deploy to AWS and will use Terratest to ensure that the following:

  • API Gateway WebSocket: Confirm that the webSocket endpoint is correctly configured for real-time communication.
  • DynamoDB Table: Verify that the key-value store for connections is created and operational.
  • IAM Roles: Ensure that permissions for interacting with AWS services are correctly set up.

Let's start by creating a new project for our application.

nitric new my-websocket-app go-starter

Application code

Replace the contents of services\hello.go with our websockets application.

package main

import (
	"context"
	"fmt"

	"github.com/nitrictech/go-sdk/handler"
	"github.com/nitrictech/go-sdk/nitric"
)

func main() {
	// Create a WebSocket endpoint named "public".
	ws, err := nitric.NewWebsocket("public")
	if err != nil {
		fmt.Println("Error creating WebSocket:", err)
		return
	}

	// Initialize a KV store named "connections" with Get, Set, and Delete permissions.
	connections, err := nitric.NewKv("connections").Allow(nitric.KvStoreGet, nitric.KvStoreSet, nitric.KvStoreDelete)
	if err != nil {
		fmt.Println("Error creating KV store:", err)
		return
	}

	// Handle new WebSocket connections by storing the connection ID in the KV store.
	ws.On(handler.WebsocketConnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) {
		err := connections.Set(context.TODO(), ctx.Request.ConnectionID(), map[string]interface{}{
			"connectionId": ctx.Request.ConnectionID(),
		})
		if err != nil {
			return ctx, err
		}

		return next(ctx)
	})

	// Handle WebSocket disconnections by removing the connection ID from the KV store.
	ws.On(handler.WebsocketDisconnect, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) {
		err := connections.Delete(context.TODO(), ctx.Request.ConnectionID())
		if err != nil {
			return ctx, err
		}

		return next(ctx)
	})

	// Handle incoming messages by broadcasting them to all other connections.
	ws.On(handler.WebsocketMessage, func(ctx *handler.WebsocketContext, next handler.WebsocketHandler) (*handler.WebsocketContext, error) {
		connectionStream, err := connections.Keys(context.TODO())
		if err != nil {
			return ctx, err
		}

		senderId := ctx.Request.ConnectionID()

		for {
			connectionId, err := connectionStream.Recv()
			if err != nil {
				break
			}

			if connectionId == senderId {
				continue
			}

			message := fmt.Sprintf("%s: %s", senderId, ctx.Request.Message())
			err = ws.Send(context.TODO(), connectionId, []byte(message))
			if err != nil {
				return ctx, err
			}
		}

		return next(ctx)
	})

	// Start the Nitric service to handle WebSocket events.
	if err := nitric.Run(); err != nil {
		fmt.Println("Error running Nitric service:", err)
	}
}

Deploying to AWS with a Terraform provider

To deploy your application with Terraform you'll need to use Nitric's Terraform providers. You can learn more about using Nitric with Terraform here.

nitric stack new dev aws-tf

Update this newly created stack file to include your target region:

# The nitric provider to use
provider: nitric/awstf@1.11.6

# The target aws region to deploy to
region: us-east-2

The Nitric Terraform providers are currently in preview, to enable them you'll need to enable beta-providers in your Nitric project. You can do this by adding the following to your project's nitric.yaml file:

preview:
  - beta-providers

Once you've created your Nitric stack, you can generate the Terraform code by running the following command:

nitric up

This will generate the Terraform code for your Nitric application into a folder named cdktf.out by default.

Add and execute Terratest

Add the necessary dependencies for Terratest:

go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/stretchr/testify/assert
go get github.com/aws/aws-sdk-go/aws
go get github.com/aws/aws-sdk-go/aws/session
go get github.com/aws/aws-sdk-go/service/apigatewayv2
go get github.com/aws/aws-sdk-go/service/dynamodb
go get github.com/aws/aws-sdk-go/service/iam

Create the Test File

Create a new file named test_terraform_resources.go in your project’s test directory:

mkdir -p test
touch test/test_terraform_resources.go

Add the following code to test_terraform_resources.go:

package test

import (
	"testing"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/apigatewayv2"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

func TestTerraformResources(t *testing.T) {
	// Set Terraform options, specifying the directory with your Terraform configuration
	terraformOptions := &terraform.Options{
		TerraformDir: "../cdktf.out/stacks/go-realtime-dev",
	}

	// Ensure resources are destroyed after test completion
	defer terraform.Destroy(t, terraformOptions)

	// Initialize and apply the Terraform configuration
	terraform.InitAndApply(t, terraformOptions)

	// Initialize AWS session for interacting with AWS services
	sess := session.Must(session.NewSession(&aws.Config{Region: aws.String("us-east-2")}))

	// Test DynamoDB table creation (key-value store)
	dynamoClient := dynamodb.New(sess)
	tableName := "connections" // Name of the DynamoDB table to check
	_, err := dynamoClient.DescribeTable(&dynamodb.DescribeTableInput{
		TableName: aws.String(tableName),
	})
	assert.NoError(t, err, "Expected DynamoDB table 'connections' to be created")

	// Test IAM role creation
	iamClient := iam.New(sess)
	roleName := "go-realtime_services-hello" // Name of the IAM role to check
	_, err = iamClient.GetRole(&iam.GetRoleInput{
		RoleName: aws.String(roleName),
	})
	assert.NoError(t, err, "Expected IAM role 'go-realtime_services-hello' to be created")

	// Test API gateway webSocket creation
	apiClient := apigatewayv2.New(sess)
	apiName := "public" // Name of the API Gateway WebSocket to check
	result, err := apiClient.GetApis(&apigatewayv2.GetApisInput{})
	assert.NoError(t, err)
	found := false
	for _, api := range result.Items {
		if *api.Name == apiName {
			found = true
			break
		}
	}
	assert.True(t, found, "Expected API Gateway WebSocket 'public' to be created")
}

Run the tests

To run the tests, navigate to your project’s root directory and execute the Go test command:

go test -v ./test

This will:

  1. Deploy the infrastructure using Terraform.
  2. Validate the creation of the DynamoDB table, IAM role, and API Gateway WebSocket.
  3. Clean up the infrastructure after testing.

The output should confirm the successful creation and validation of each resource.