Terraform on Azure - Testing of a Virtual Machine using Terratest

azure terraform go terratest infrastructure-as-code testing devops cloud

This post explains how to write a test for a Terraform configuration, using the Terratest framework

1. Introduction

I’ve been using Terraform for almost three years, and I’ve created a lot of configurations so far for provisioning several resources on Azure. I really like to write Infrastructure as Code, I’ve also integrated it into pipelines, and tried to ensure the quality, as best as possible. But additional verification steps for testing specific values of the resources after the deployment, were still missing.

Therefore, I started with investigations about how to create and run tests for Terraform. I came across the Terratest framework and tried a simple example. It worked well, and I started to create a test for one of my existing Terraform configurations. It’s about verifying the values of a Windows virtual machine, which will be deployed and destroyed on Azure as part of the test.

2. Setting up the environment

It is necessary to install Go for using Terratest, use a version >=1.21.1.

According to the manual of gruntwork.io, I’ve created a new directory, named “test” side-by-side to the Terraform files of my existing Terraform configuration, which is capable of deploying a Windows 11 virtual machine on Azure - see https://github.com/patkoch/iac_terraform_azure/tree/main/vm/win11

01_folder_structure

Next, I needed to configure the dependencies. This is done by executing the following commands:

 go mod init "<name of module>"
 go mod tidy

According to my repository, which is named “iac_terraform_azure”, the name of the module would be “https://github.com/patkoch/iac_terraform_azure":

02_go_init

After that, the setup is done.

3. Creating and running the Terratest for the virtual machine

3.1 The source code of the Terratest

Next, I could start with the creation of the test. Still following the manual of Gruntwork, I’ve created a file named “terraform_virtual_machine_test.go” inside the directory “test”:

03_test_in_vscode

The content of the file can be seen in the snippet below: it’s based on the example provided by Github.com - Gruntwork-io - Terratest.

package test

import (
	"net"
	"testing"

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

// Function for verifying an ip address - source: GitHubCopilot
func isValidIP(ip string) bool {
	parsedIP := net.ParseIP(ip)
	return parsedIP != nil
}

func TestDeploymentVirtualMachine(t *testing.T) {
	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		// The TerraformDir variable points the location of the Terraform configuration which shall be tested
		TerraformDir: "../",
	})

	// Conduct the destruction at the end of the test
	defer terraform.Destroy(t, terraformOptions)

	// Conduct the "init" and "apply" commands and fail in case of any errors
	terraform.InitAndApply(t, terraformOptions)

	// Verification of the virtual machine name
	realVirtualMachineName := terraform.Output(t, terraformOptions, "my_virtual_machine_name")
	assert.Equal(t, "windows11-21h2", realVirtualMachineName)

	// Verification of the resource group name
	realResourceGroupName := terraform.Output(t, terraformOptions, "my_resource_group_name")
	assert.Equal(t, "iac-azure-terraform", realResourceGroupName)

	//Verification of the resource group location
	realResourceGroupLocation := terraform.Output(t, terraformOptions, "my_resource_group_location")
	assert.Equal(t, "westeurope", realResourceGroupLocation)

	// Verification of the public ip address
	realVirtualMachineIP := terraform.Output(t, terraformOptions, "my_virtual_machine_public_ip")
	assert.True(t, isValidIP(realVirtualMachineIP))
}

3.2 Explanation of the test

This section explains the source code of the test step-by-step.

3.2.1 Function for verifying an IP address

I wanted to write a function which verifies whether a returned string is a valid IP address: here I relied on the GitHub Copilot:

04_githubcopilot

So, I was grateful for that and added this suggestion (see the snippet below) to my Terratest:

// Function for verifying an ip address - source: GitHubCopilot
func isValidIP(ip string) bool {
	parsedIP := net.ParseIP(ip)
	return parsedIP != nil
}

3.2.2 The function header, the WithDefaultRetryableErrors function and the TerraformDir

I named the whole function “TestDeploymentVirtualMachine”, the first line of it contains a call to the function “WithDefaultRetryableErrors”. That’s an already existing function of the Terratest library. The idea of that function is to enable a repeatable call of Terraform commands in case of some errors. In case of an error (e.g., network), the Terraform command will be applied again.

How does the function know which errors to handle? There exists a specific field within the “terraformOptions” struct, which allows to the provision of several error strings. If such an error string appears, then the dedicated Terraform command will be executed again.

The variable “TerraformDir” needs to point to the directory in which the Terraform files are located. In my case, it’s one directory level above - as the directory “test” exists side-by-side to my Terraform files.

func TestDeploymentVirtualMachine(t *testing.T) {
	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		// The TerraformDir variable points the location of the Terraform configuration which shall be tested
		TerraformDir: "../",
	})

3.2.3 Destruction of the resources

The following function will be executed after running the test (doing the init and the apply) to destroy the resources.

defer terraform.Destroy(t, terraformOptions)

3.2.4 Provisioning of the resources

The provisioning of the resources will be done with the call of the function “InitAndApply”, as the name already suggests:

terraform.InitAndApply(t, terraformOptions)

3.2.5 Verification of output values

That’s probably the most interesting part, in which I can verify specific values of the provisioned virtual machine. I decided to prove the following values of the virtual machine:

For that, I define for each value a corresponding “output” variable in my “outputs.tf” file:

05_outputs

Back at the test, I’m going to call the “terraform.Output” function for each defined output variable to get the real value after the deployment of the virtual machine. Finally, I’ll do the verification with an “assert.Equal” call. The IP address will be verified with the mentioned “isValidIP” function, which GitHub Copilot provided me:

// Verification of the virtual machine name
realVirtualMachineName := terraform.Output(t, terraformOptions, "my_virtual_machine_name")
assert.Equal(t, "windows11-21h2", realVirtualMachineName)

// Verification of the resource group name
realResourceGroupName := terraform.Output(t, terraformOptions, "my_resource_group_name")
assert.Equal(t, "iac-azure-terraform", realResourceGroupName)

//Verification of the resource group location
realResourceGroupLocation := terraform.Output(t, terraformOptions, "my_resource_group_location")
assert.Equal(t, "westeurope", realResourceGroupLocation)

// Verification of the public ip address
realVirtualMachineIP := terraform.Output(t, terraformOptions, "my_virtual_machine_public_ip")
assert.True(t, isValidIP(realVirtualMachineIP))

3.3 Executing the test

To run the test, start a terminal and change the directory to the “test” directory. The test can be executed with:

go test -v -run TestDeploymentVirtualMachine

After executing that command, the test “TestDeploymentVirtualMachine” starts. The resources will be provisioned and destroyed as part of the test, including the validation of the mentioned values.

06_terratest_start_test

The four mentioned values, which will be verified by the test, are defined as outputs (see outputs.tf):

07_terratest_apply_complete

The test result will be shown after the destruction. The picture below shows that the test passed:

08_terratest_destroy_ressources

So this test allows me to verify four values. This will be done after provisioning the virtual machine by checking the output variables. The resources will be destroyed after the verification.

4. Conclusion

The installation of Go, and the setup of the project could be carried out quickly. Based on the existing manuals, and the documentation, I was able to create the test easily. As a prerequisite, I needed to add outputs to my already existing Terraform configuration. These values, which are defined in my “outputs.tf” file, will be used to be verified after the deployment of the resources. In the case of a virtual machine, meaningful values for that would be e.g.:

Already existing functions of the Terratest framework ensure that the Terraform commands will be executed again in case of an error.

5. References

https://terratest.gruntwork.io/

https://golang.org

https://terratest.gruntwork.io/docs/getting-started/quick-start/

https://github.com/gruntwork-io/terratest/tree/master/examples/terraform-hello-world-example

https://www.terraform.io/

https://azure.microsoft.com/