Azure, Azure DevOps, DevOps, Professional, YAML Pipelines

An Azure Load Testing CI/CD Template

Introduction

In this post will cover how we can incorporate Azure Load Testing into our Continuous Delivery/Continuous Integration Pipelines. At the end of this post there will be a complete YAML templated stage responsible for executing your Azure Load Tests, something that should be easily inserted into any existing CI/CD Pipeline.

If you’d like to jump to the code it is available on the GitHub repo TheYAMLPipelineOne. This process is referenced in my presentation to the West Michigan Azure User Group and available to watch here.

Background

Per Microsoft’s own documentation Azure Load Testing is described as:

generate high-scale load and run simulations with a fully managed load-testing service, built for Azure. Create tests quickly without knowledge of load-testing tools, or upload your existing Apache JMeter scripts. Gain actionable insights into performance, scalability, and capacity and support continuous improvement through automated continuous integration and continuous delivery (CI/CD) workflows.

https://azure.microsoft.com/en-us/products/load-testing/

This sounds awesome! Not only does this service help facilitate the DevOps need to build a better quality product faster, it already leverages the Azure native services!

For the YAML Pipeline post if you haven’t already I’d advise you to check out my posts on this topic to get a better understanding on how I approach creating YAML templates for CI/CD.

PreReqs

Architecture

With this template I have decided to take the approach that Azure Load Testing will be it’s own stage in ADO. This stage will be tied to it’s own load testing environment for approvals. The rationale behind this is essentially creating an optional Load Testing stage that can be over written by a simple true/false statement. Additionally by doing this at the stage level one can easily insert this into any existing pipeline. Alternatively if we’d rather use the job and task both will be templated out to be piecemealed.

I’ve also included just some basic guidance on how to configure and access require for the ADO Service Account.

Task

Azure Load Testing has it’s own dedicated Azure DevOps Task. There is also a GitHub one; however, we will be focusing on the ADO version.

This task is pretty straightforward; however, to optimize for reuse it’s usually a good idea to provide parameters with defaulted values.

azure_load_test_task.yml

parameters:
- name: azureSubscriptionName
  type: string
  default: ''
- name: loadTestConfigFile
  type: string
  default: ''
- name: loadTestResourceGroupName
  type: string
  default: ''
- name: loadTestResourceName
  type: string
  default: ''
- name: secretsJSONObject
  type: string
  default: ''
- name: envJSONObject
  type: string
  default: ''

steps:
- task: AzureLoadTest@1
  inputs:
    azureSubscription: ${{ parameters.azureSubscriptionName }}
    loadTestConfigFile: ${{ parameters.loadTestConfigFile }}
    resourceGroup: ${{ parameters.loadTestResourceGroupName }}
    loadTestResource: ${{ parameters.loadTestResourceName }}
    secretsJSONObject: ${{ parameters.secretsJSONObject }}
    secrets: ${{ parameters.secretsJSONObject }}
    env: ${{ parameters.envJSONObject }}

Job

There can be additional tasks; however, for our purposes we will just be calling the above task. I am a proponent of even templating one task jobs as this will just optimize and provide for future flexibility. In this template we are leveraging a separate variable file to load the specifics azure environment information such as the Service Connection to use.

load_test_execute_job.yml

parameters:
- name: loadTestConfigFile
  type: string
  default: ''
- name: loadTestResourceName
  type: string
  default: ''
- name: loadTestResourceGroupName
  type: string
  default: ''
- name: secretsJSONObject
  type: string
  default: ''
- name: envJSONObject
  type: string
  default: ''
- name: regionAbrv
  type: string
  default: ''
- name: environmentName
  type: string
  default: ''
  
jobs:
- deployment: 'run_azure_load_test_${{ parameters.environmentName }}_${{ parameters.regionAbrv }}'
  environment: ${{ parameters.environmentName }}_loadtest
  variables: 
  - template: ../variables/azure_${{parameters.environmentName}}_variables.yml
  strategy:
    runOnce:
        deploy:
            steps:
            - template: ../tasks/azure_load_test_task.yml
              parameters:
                azureSubscriptionName: ${{ variables.azureServiceConnectionName}}
                loadTestConfigFile: ${{ parameters.loadTestConfigFile }}
                loadTestResourceGroupName: ${{ parameters.loadTestResourceGroupName }}
                loadTestResourceName: ${{ parameters.loadTestResourceName }}
                secretsJSONObject: ${{ parameters.secretsJSONObject }}
                envJSONObject: ${{ parameters.envJSONObject }}

Stage

Lastly we have the stage that will be called by the pipeline. This stage is just the next building block from the job above. There really isn’t anything out of the order in it.

load_test_stage.yml

parameters:
- name: environmentName
  type: string
  default: 'tst'
- name: regionAbrv
  type: string
  default: ''
- name: loadTestConfigFile
  type: string
  default: ''
- name: secretsJSONObject
  type: string
  default: ''
- name: envJSONObject
  type: string
  default: ''
- name: loadTestResourceName
  type: string
  default: ''
- name: loadTestResourceGroupName
  type: string
  default: ''
- name: serviceName
  type: string
  default: ''

stages:
- stage: '${{ parameters.serviceName }}_${{ parameters.environmentName}}_${{parameters.regionAbrv}}_load_test'
  jobs:
  - template: ../jobs/load_test_execute_job.yml
    parameters:
      environmentName: ${{ parameters.environmentName }}
      loadTestConfigFile: ${{ parameters.loadTestConfigFile }}
      secretsJSONObject: ${{ parameters.secretsJSONObject }}
      envJSONObject: ${{ parameters.envJSONObject}}
      regionAbrv: ${{ parameters.regionAbrv }}
      loadTestResourceGroupName: ${{ parameters.loadTestResourceGroupName }}
      loadTestResourceName: ${{ parameters.loadTestResourceName }}

Pipeline

For the calling Pipeline I have configured a repository, LoadTestingDemo, which will call this stage. Below is a snippet on how to do such an action. I do have an expression to detect a.) if load test is true and b.) add it after the tst stage.

loadtestingdemo_template.yml

- ${{ if eq(parameters.runAzureLoadTest, 'true') }} :
    - ${{ each environmentObject in parameters.environmentObjects }} :
      - ${{ if eq(environmentObject.environmentName, 'tst')}}:
        - ${{ each regionAbrv in environmentObject.regionAbrvs }} :
          - template: stages/load_test_stage.yml@templates
            parameters:
              environmentName: ${{ environmentObject.environmentName }}
              regionAbrv: ${{ regionAbrv }}
              loadTestConfigFile: 'tests/SampleApp.yaml'
              serviceName: ${{ parameters.serviceName }}
              envJSONObject: '
                  [
                    {
                    "name": "webapp",
                    "value": "$(webAppName).azurewebsites.net"
                    }
                  ]'
              loadTestResourceName: 'lt-test-dev-eus'
              loadTestResourceGroupName: 'rg-test-dev-eus'
    

Conclusion

There it is! My intention is to package this in such a way that any organization can consume and leverage for their Azure Load Tests. We’ve just create not only a task template, but a job, and a stage template. All of which can be leverage in your organization. The complete YAML template as well as additionaly templates can be found on my GitHub Repo TheYAMLPipelineOne.