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
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":
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”:
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:
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:
- the resource group location
- the resource group name
- the name of the virtual machine
- the public IP address
For that, I define for each value a corresponding “output” variable in my “outputs.tf” file:
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.
The four mentioned values, which will be verified by the test, are defined as outputs (see outputs.tf):
The test result will be shown after the destruction. The picture below shows that the test passed:
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.:
- the resource group location
- the resource group name
- the name of the virtual machine
- the public IP address
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://terratest.gruntwork.io/docs/getting-started/quick-start/
https://github.com/gruntwork-io/terratest/tree/master/examples/terraform-hello-world-example