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
- Create and set up your application.
- Deploying to AWS with a Terraform provider.
- 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. You can change this via the outdir property and inspect the code before deploying.
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:
- Deploy the infrastructure using Terraform.
- Validate the creation of the DynamoDB table, IAM role, and API Gateway WebSocket.
- Clean up the infrastructure after testing.
The output should confirm the successful creation and validation of each resource.