From Zero to Virtual Network with Terraform and Terratest: A Step-by-Step Guide Part 2
In the previous blog, we created vnet and subnet using Terraform. In this blog, we will be testing those resources using Terratest. I will recommend to go through that blog once, it will help you to understand the flow.
Here is the link to the previous blog https://saumyapandey.hashnode.dev/from-zero-to-virtual-network-with-terraform-and-terratest-a-step-by-step-guide-part-1
What is Terratest ?
Terratest is an open-source testing framework
for the infrastructure as code. It allows users to write automated tests in Go
to verify that their infrastructure code is working correctly.
Terratest provides a variety of helper functions and utilities to make it easier to write tests, including functions to create and destroy resources, check resource state, and run commands on resources.
Terratest supports testing for a variety of infrastructure platforms, including Amazon Web Services (AWS), Google Cloud Platform (GCP), Microsoft Azure, Kubernetes, and Docker
.
Terratest is maintained by Gruntwork, a company that provides DevOps-as-a-Service and Infrastructure-as-Code consulting services.
Here are the 4 steps involves in terratest infrastructure testing .
How to create a test file for terraform resources?
Terratest uses the Go testing framework. To use Terratest, you need to install, The version of Go
should be >=1.18
. Installation Guide
When creating a test file for a Go project, it is a convention to name the file with a suffix of _test.go
. This naming convention allows Go's testing framework to automatically detect and run the tests in the file. Therefore, it is recommended to follow this convention and name your test file with a suffix of _test.go
.
Here is the directory structure for Terraform and terratest. Terraform code resides in the module directory whereas terratest code resides in the test directory.
Note: Follow Part 1 of this blog as well to create all the files and run the unit test for this vnet subnet module using Terratest.
Copy the following content in vnet_test.go
file
package test
import (
//"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"github.com/gruntwork-io/terratest/modules/azure"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
)
func TestAzureVirtualNetwork(t *testing.T) {
// Run test parallely
t.Parallel()
// Load environment variables from .env file
err := godotenv.Load()
if err != nil {
t.Fatalf("Error loading .env file: %v", err)
}
AZURE_REGION := os.Getenv("AZURE_REGION")
RESOURCE_GROUP_NAME := os.Getenv("RESOURCE_GROUP_NAME")
CREATE_RG := os.Getenv("CREATE_RG")
CREATE_VNET := os.Getenv("CREATE_VNET")
VNET_NAME := os.Getenv("VNET_NAME")
NETWORK_ADDRESS_IP := os.Getenv("NETWORK_ADDRESS_IP")
NETWORK_ADDRESS_MASK := os.Getenv("NETWORK_ADDRESS_MASK")
JSON_STR := os.Getenv("SUBNET_CONFIG")
dns_entries_str := os.Getenv("DNS_ENTRIES")
dns_entries := strings.Split(dns_entries_str, ",")
NSG := ("NSG")
var SUBNET_CONFIG map[string]map[string]interface{}
err = json.Unmarshal([]byte(JSON_STR), &SUBNET_CONFIG)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(SUBNET_CONFIG)
// Set the Azure Region and Resource Group name
resourceGroupName := fmt.Sprintf("%s-%s", RESOURCE_GROUP_NAME, random.UniqueId())
vnet_name := fmt.Sprintf("%s-%s", VNET_NAME, random.UniqueId())
//Configure the Terraform options with input variables
terraformModulePath := "../module/"
//Set the input variables for the Terraform code
terraformVars := map[string]interface{}{
"location": AZURE_REGION,
"create_rg": CREATE_RG,
"virtual_network_rg_name": resourceGroupName,
"create_vnet": CREATE_VNET,
"virtual_network_name": vnet_name,
"network_address_ip": NETWORK_ADDRESS_IP,
"network_address_mask": NETWORK_ADDRESS_MASK,
"subnet_config" : SUBNET_CONFIG,
"dns_entries": []string{dns_entries[0], dns_entries[1]},
"nsg": NSG ,
}
// Terraform options with retries and timeout
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: terraformModulePath,
Vars: terraformVars,
NoColor: true,
})
// Apply the Terraform code
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get the subnet names from the Terraform output
subnetNames := terraform.OutputMap(t, terraformOptions, "subnet_details")
subscriptionID := terraform.Output(t, terraformOptions, "current_subscription_id")
subnet_ids_nsg := terraform.OutputList(t, terraformOptions, "subnet_ids_nsg")
subnet_names := terraform.OutputList(t, terraformOptions, "subnet_names")
t.Run("VirtualNetwork_Subnet", func(t *testing.T) {
// Check the Subnet exists in the Virtual Network Subnets with the expected Address Prefix
actualVnetSubnets := azure.GetVirtualNetworkSubnets(t, vnet_name, resourceGroupName, subscriptionID)
assert.NotNil(t, actualVnetSubnets)
assert.Equal(t, subnetNames, actualVnetSubnets)
})
t.Run("Exists", func(t *testing.T) {
// Check the Virtual Network exists
assert.True(t, azure.VirtualNetworkExists(t, vnet_name, resourceGroupName, subscriptionID))
})
t.Run("NSG Exists", func(t *testing.T) {
// Check the Virtual Network exists
addString := fmt.Sprintf("/%s/%s/%s/%s/%s/%s/%s/%s/%s/", "subscriptions", subscriptionID, "resourceGroups",resourceGroupName, "providers", "Microsoft.Network", "virtualNetworks" , vnet_name, "subnets")
newList := []string{}
for _, s := range subnet_names {
newList = append(newList, addString+s)
}
fmt.Println(newList)
assert.ElementsMatch(t, newList, subnet_ids_nsg)
})
}
This is a Go test file that contains the test code for testing an Azure Virtual Network using Terraform. Let's go through the different parts of the code:
The first line defines the package name, which is "test"
. A package in Go is a collection of related Go files that can be compiled and used together.
The import
statements load the necessary Go packages for the test code to function. The packages loaded include:
"encoding/json"
for encoding and decoding JSON data"fmt"
for formatting text output"os"
for accessing environment variables"strings"
for manipulating strings"testing"
for running tests"
github.com/gruntwork-io/terratest/modules/azure
"
for accessing Azure resources using Terratest"
github.com/gruntwork-io/terratest/modules/random
"
for generating random IDs for Azure resources"
github.com/gruntwork-io/terratest/modules/terraform
"
for running Terraform code using Terratest"
github.com/joho/godotenv
"
for loading environment variables from a .env file"
github.com/stretchr/testify/assert
"
for making assertions in the test code.
The function TestAzureVirtualNetwork
is the main function that contains the test code. This function takes a pointer to the testing.T struct,
which provides methods for running tests and logging errors.
The t.Parallel()
call enables running the tests in parallel, which can help speed up the test suite.
The err := godotenv.Load()
call loads environment variables from a .env file using the godotenv package. If the file is not found, the test fails.
The os.Getenv
calls load the environment variables needed for the test from the .env file. These variables include the Azure region, resource group name, virtual network name, IP address and mask, DNS entries, and NSG.
The SUBNET_CONFIG
environment variable is a JSON string that contains a map of subnet names to their respective configurations. The json.Unmarshal
call decodes the JSON string into a Go map.
The resourceGroupName
and vnet_name
variables are generated by appending a unique ID to the input resource group name and virtual network name using the random.UniqueId()
function.
The terraformModulePath
variable defines the path to the Terraform module being tested.
The terraformVars
map defines the input variables for the Terraform module being tested.
The terraformOptions
variable is a Terraform Options struct that specifies the Terraform directory, input variables, and retry and timeout options.
The terraform.InitAndApply
function applies the Terraform code, while the terraform.Destroy
function destroys the created resources after the test is complete.
The t.Run
function runs a subtest within the main test. The first subtest checks if the created subnet exists in the virtual network subnets with the expected address prefix. The azure.GetVirtualNetworkSubnets
function retrieves the subnets from the virtual network and the assert
package checks that the retrieved subnet names match the expected subnet names.
The second subtest checks if the virtual network exists. The azure.VirtualNetworkExists
function checks if the virtual network exists in Azure, and the assert.True
function checks that the function returns true.
The third subtest checks if the NSG exists for the virtual network subnets. It builds a list of expected subnet IDs based on the virtual network and subnet names, and checks that the retrieved subnet IDs match the expected subnet IDs using the assert.ElementsMatch
function.
Overall, this test file uses Terratest and Go to test a Terraform module that creates an Azure Virtual Network with subnets and NSGs, and checks that the created resources exist and with the expected configurations.
Create .env file
Create .env
file and paste the following contents. The .env file contains all the environment variables of the code. It's important to note that you should avoid including sensitive information in your code repository whenever possible. It is not recommended to include sensitive information like secrets, keys, and tokens in your code repository. Instead, you should store them securely in a separate location and access them using environment variables or other secure methods.
RESOURCE_GROUP_NAME=vnet-rg
VNET_NAME=vnet-test
AZURE_REGION=uksouth
CREATE_RG=true
CREATE_VNET=true
NETWORK_ADDRESS_IP=10.0.0.0
NETWORK_ADDRESS_MASK=16
SUBNET_CONFIG="{\"subnet1\":{\"name\":\"subnet1\",\"cidr_base\":\"10.0.1.0\",\"mask\":24},\"subnet2\":{\"name\":\"subnet2\",\"cidr_base\":\"10.0.2.0\",\"mask\":24}}"
DNS_ENTRIES=10.0.0.4,10.0.0.5
NSG=test-nsg
You can change these values according to your use case and requirements
How to run the test in Terratest ?
To run the test go to the test folder using cd test
.
go mod init test
This command initializes a new module in the current directory with the name test
. A module is a collection of Go packages that are versioned together.
go mod tidy
This command removes unused dependencies from the go.mod
file and downloads the packages needed to build the module. It also updates the go.sum
file to include the latest checksums of the downloaded packages. and now the folder will look something like this.
go test -v
This command runs all tests in the current directory and its subdirectories. The -v
flag makes the output verbose, which means it displays more details about the tests being run, such as the names of the tests and whether they passed or failed.
You can run the tests using any of the following commands as well if you want more control :
go test -v -run TestAzureVirtualNetwork
: This command runs theTestAzureVirtualNetwork
test in verbose mode. The-run
flag filters the tests that are run based on the regular expression that follows it. In this case, it runs only the test that matches the nameTestAzureVirtualNetwork
.go test -v -run TestAzureVirtualNetwork -timeout 30m
: This command runs theTestAzureVirtualNetwork
test with a timeout of 30 minutes. The-timeout
flag sets the maximum amount of time a test is allowed to run before it is considered to have failed.go test -v -run TestAzureVirtualNetwork -timeout 30m -parallel 1
: This command runs theTestAzureVirtualNetwork
test with a timeout of 30 minutes and a maximum parallelism of 1. The-parallel
flag sets the maximum number of tests that can run in parallel. In this case, only one test can run at a time. This is useful when running tests that require shared resources, such as a database or a network.
And in this way you can write unit test cases for each terraform modules and test those modules to verify that their infrastructure code is creating resources with the correct configuration, those resources are accessible and working as intended, and that updates to infrastructure code do not cause any unintended consequences.
I hope you liked this blog. Let me know in the comment if you want me to write blog on a particular topic.
Resources :
Subscribe to my newsletter
Read articles from DevOps Talks directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
DevOps Talks
DevOps Talks
Hey there, I'm the DevOps wizard with a passion for automating everything in sight. When I'm not knee-deep in code, I love to explore the latest tech trends and listen to my favorite tunes. With my keen attention to detail and problem-solving skills, I'm the go-to person for any infrastructure challenge or automation.