Test configuration in Azure DevOps CI/CD pipeline with use of Pester framework

Posted by Nikos Tsirmirakis on 2020-05-05

In my previous post, I have described how to create an Azure DevOps pipeline to build and destroy the SQL server. Now I will describe how to add and run tests as part of the deployment pipeline with use of Pester module. All scripts are available in DBAinTheCloud, Github repository.

Pester PowerShell module is a test framework (home page and GitHub repository) you can use for writing tests in TDD (test-driven development) environment. I will describe how to use it for running infrastructure and configuration tests.

There are 3 types of tests we can run depending on connectivity requirements.

Test type Agent Subscription access Internet access Local network access
Azure infrastructure Any (hosted or local) Yes Yes No
Internet-facing features Hosted No* Yes No
Internal server configuration Local build or deployment agent No* No* Yes

/* Subscription access is not required for running the test, however, we might need to query Azure for object details to test, like a public IP address.

We will run the following tests mainly focusing on Azure infrastructure.

Tests to run after the build

  • Resource group existence
  • Server name (d1-dm3-sql*)
  • Servers count
  • Servers size
  • NSG rule to allow RDP access from specified IP
  • to a public IP address on tcp\3389 (RDP) from internet

Test to run after destroy

  • Resource group, not existence

Testing scripts

To run a pester tests we will create a PowerShell launcher script which will run the tests (invoke-pester) and test definition files (*.tests.ps1) including all the tests. Scripts are available in this GitHub repository.

Invoke script

Following script (run-tests-after-build.ps1) is running tests and generating output with tests results in NUnit format.


$path = "$($env:system_DefaultWorkingDirectory)\tests\invoke-and-tests\"

Invoke-Pester -OutputFile "$($path)\tests-after-build-results.xml" `
-OutputFormat 'NUnitXml' `
-Script "$($path)\tests-after-build.tests.ps1"

Invoke-Pester -OutputFile "$($path)\tests-connectivity-from-internet.xml" `
-OutputFormat 'NUnitXml' `
-Script "$($path)\test-connectivity-from-internet.tests.ps1"

Tests definition

Following script (tests-after-build.tests.ps1) contains tests definition which we are passing as a parameter in pester-invoke step.


$rg_name = "A-D1-DM3-RG"
$nsg_name = "A-D1-DM3-nsg"
$allowed_public_IP = "?.?.?.?"
$server_names = ("a-d1-dm3-sql1")
$server_size = "Standard_DS2_v2"
$server_count = $server_names.count

describe 'Azure congifuration test' {

    context "Check if resource group exist" {

        $rg_exist = ((Get-AzResourceGroup | where { $_.ResourceGroupName -eq $rg_name }).count -eq 1)

        it "Resource group ($($rg_name)) exist" {
            $rg_exist | should be $true
        }
    }

    context "Check NSG rules - if allow access from home IP exist" {

        $NSG = Get-AzNetworkSecurityGroup -name $nsg_name -ResourceGroupName $rg_name

        $access = ($NSG.SecurityRules | where-object { ($_.Direction -eq "Inbound") -and ($_.SourceAddressPrefix -contains $allowed_public_IP) -and ($_.DestinationPortRange -contains "3389") }).access

        It "$($NSG.name) allows access from $($allowed_public_IP)" {
            $access | Should Be 'Allow'
        }
    }

    context "Server(s) check" {

        foreach ($server in $server_names) {

            # check if server exist
            $server_exist = (Get-AzVM | where { ($_.ResourceGroupName -eq $rg_name) -and ($_.Name -eq $server) }).count

            it "Server $($server) does exist" {
                $server_exist | should be 1
            }
            
            # check server size
            $server_size_test = (Get-AzVM -ResourceGroupName $rg_name -Name $server).HardwareProfile.VmSize

            it "Server $($server) size is $($server_size)" {
                $server_size_test | should be $server_size
            }

        }
    }

    context "Server(s) count check" {
        
        # check server count in RG
        $server_counts_test = (Get-AzVM -ResourceGroupName $rg_name).count

        it "Server(s) count is $($server_count)" {
            $server_counts_test | should be $server_count
        }
    }
}

Pipeline (build)

Now when we have all our scripts ready we have to create a build for it so we can use it as artifact in our deployment pipeline.

Release

Release pipeline we will clone release pipeline from demo 3 which we used in the previous post and we will add additional steps to download tests scripts and test definitions, run the tests and publish the results.

Additional artifact to download

In this step, we are downloading artifact with PowerShell scripts.

Unzip

In this step, we are extracting PowerShell scripts.

Run tests

In this step, we are running the tests. We are running it as an Azure PowerShell and we can execute it in against our subscription.

Publish results

In this step, we are publishing test results to Azure DevOps.

In destroying job we will use the same steps with only one exception, in running tests step we will run run-tests-after-destroy.ps1 instead of run-tests-after-build.ps1 script.

Congratulations!

We have run tests against our environment after each stage.

Coming next …

In my next post, I will describe how to use Azure VM extensions in Terraform to customise VM further.