Testing terraform with moto, part 3 - terratest

April 25, 2022   

In my last post I ran into a roadblock related to the moto project, and it’s API responses not matching those of AWS, causing terraform to fail.

Since posting that blog post and the issue, moto contributor bblommers responded to and fixed my issue (pretty darn fast actually), and so I was able to reproduce my experiment with the latest release of moto, and it works just fine.

In this post we will explore the basics of testing out our module with terratest.

What is terratest?

“Terratest is a Go library that provides patterns and helper functions for testing infrastructure, with 1st-class support for Terraform”

Terratest homepage

Think of it as a collection of pre-defined test helpers, built on go’s testing framework, designed to reduce repetitive automation and writing of assertions, entirely focused on infrastructure automation. It means you don’t have to write code for the majority of handling terraform, AWS APIs, etc. Instead, terratest gives you lots of this ready to go, and allows you to focus on expressing your infrastructure tests instead of writing lower level code.

For our purposes, the helpers around terraform automate the running of terraform plans, parsing the output, and cleaning up afterwards. Later on, we’ll explore if we can also verify the resources created against AWS API

Let’s get started

So, following the quickstart applied to our repo from part 1, we’ll create the following files:

1+-- tests/
2    +-- examples/
3        +-- test.tf # this will contain the root module we will test, importing our module under test
4    +-- kubernetes-aws.go # this will contain the go code that runs, asserts and cleans the test environment

I’ve created my tests/examples/test.tf like so:

 1// setup provider for localstack
 2provider "aws" {
 3    region  = "us-east-1"
 4    access_key                  = "test"
 5    secret_key                  = "test"
 6    s3_use_path_style           = true
 7    skip_credentials_validation = true
 8    skip_metadata_api_check     = true
 9    skip_requesting_account_id  = true
10
11    // override endpoints to point to localstack
12    endpoints {
13        ec2            = "http://localhost:5000"
14        eks            = "http://localhost:5000"
15        iam            = "http://localhost:5000"
16        autoscaling    = "http://localhost:5000"
17    }
18}
19
20// use to get the VPC from moto
21data "aws_vpc" "default" {
22}
23data "aws_subnets" "default" {
24    filter {
25        name   = "vpc-id"
26        values = [data.aws_vpc.default.id]
27    }
28}
29
30// configure the module with some example values,
31module "kubernetes-aws" {
32  source            = "../../../modules/kubernetes-aws"
33  cluster_name      = "kubernetes-test"
34  cluster_version   = "1.21"
35  project           = "terraform-testing"
36  environment       = "test"
37  min_workers       = 1
38  max_workers       = 6
39  desired_workers   = 3
40  instance_type     = "t3.large"
41  is_testing        = true
42
43  // connect up to pre-existing vpc and subnet
44  vpc_id            = data.aws_vpc.default.id
45  subnets           = data.aws_subnets.default.ids
46}
47
48
49// output some useful values
50output "name" {
51    value = module.kubernetes-aws.name
52}
53output "endpoint" {
54    value = module.kubernetes-aws.endpoint
55}
56output "user_arn" {
57    value = module.kubernetes-aws.aws_iam_role_user_arn
58}

and tests/kubernetes-aws_test.go contains (mostly from the getting started tutorial):

 1package test
 2
 3import (
 4	"testing"
 5
 6	"github.com/gruntwork-io/terratest/modules/terraform"
 7	"github.com/stretchr/testify/assert"
 8)
 9
10func TestTerraformKubernetesAwsModule(t *testing.T) {
11	// retryable errors in terraform testing.
12	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
13		TerraformDir: "examples/kubernetes-aws",
14	})
15
16	defer terraform.Destroy(t, terraformOptions)
17
18	terraform.InitAndApply(t, terraformOptions)
19
20	name := terraform.Output(t, terraformOptions, "name")
21	endpoint := terraform.Output(t, terraformOptions, "endpoint")
22	user_arn := terraform.Output(t, terraformOptions, "user_arn")
23	assert.Equal(t, name, "kubernetes-test")
24	assert.Contains(t, endpoint, "us-east-1.eks.amazonaws.com")
25	assert.Contains(t, user_arn, "role/kubernetes-test--AmazonEKSUserRole")
26}

Then I created a little Makefile target that runs moto locally, and then runs the tests:

1test-infra:
2	docker stop kubernetes-cluster-provisioning-test || true
3	docker run --rm -d --name kubernetes-cluster-provisioning-test -p 5000:5000 motoserver/moto:latest
4	cd tests && go test -v -timeout 30m

run make test-infra and:

1<snip a lot of terraform output>
2--- PASS: TestTerraformKubernetesAwsModule (45.62s)
3PASS
4ok  	github.com/elifesciences/kubernetes-cluster-provisioning/v2	45.854s

πŸŽ‰πŸ’ƒπŸ•ΊπŸŽ‰

Remember - this is running against the local moto (disconnect from your AWS account to be sure). It also runs faster than against AWS, and obviously saves money.

Next time, we will look into using more examples and assertions against the moto API to verify if the resources have been created correctly.