From Zero to Virtual Network with Terraform and Terratest: A Step-by-Step Guide Part 2

DevOps TalksDevOps Talks
8 min read

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:

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 :

  1. go test -v -run TestAzureVirtualNetwork: This command runs the TestAzureVirtualNetwork 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 name TestAzureVirtualNetwork.

  2. go test -v -run TestAzureVirtualNetwork -timeout 30m: This command runs the TestAzureVirtualNetwork 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.

  3. go test -v -run TestAzureVirtualNetwork -timeout 30m -parallel 1: This command runs the TestAzureVirtualNetwork 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 :

10
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.