ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Pipelines で Azure Load Testing のパイプラインを構築する

Azure Load Testing は、Apache JMeter ベースのマネージドな負荷テストサービスです。
docs.microsoft.com
過去に Azure DevOps で提供されていた負荷テスト機能がなくなって以来、ようやく Azure でマネージドなサービスとして提供されました。
今回は、Azure Pipelines を使って負荷テストを自動化してみます。

Azure Load Testing を作成する

公式ドキュメントのクイックスタートに従って、Azure ポータルから作ります。
docs.microsoft.com

注意点は、リソースを作成した後にアクセスコントロールを設定する必要がある部分です。Load Test Contributor もしくは Load Test Owner のロールをアサインしておきましょう。
また、負荷テストの対象として、Azure Web Apps も作成しておきました。

パイプラインを作成する

公式ドキュメントのチュートリアルに従って、Azure DevOps から作ります。
docs.microsoft.com

Azure DevOps Marketplace から Azure Load Testing task extension のインストールと、アクセスコントロールの設定を忘れずに行いましょう。
再利用性を考慮し、下記のようなディレクトリ構成にしました。

f:id:TonyTonyKun:20211219133547p:plain

Azure Load Testing のタスクを実行する load-test-pipelines.yml です。

parameters:
  imageName: ''
  azureSubscription: ''
  loadTestConfigFile: ''
  resourceGroup: ''
  loadTestResource: ''

jobs:
- job: LoadTest
  displayName: 'Load Test'
  pool:
    vmImage: ${{parameters.imageName}}
  steps:
  - task: AzureLoadTest@1
    displayName: 'Run Azure Load Testing'
    inputs:
      azureSubscription: ${{parameters.azureSubscription}}
      loadTestConfigFile: ${{parameters.loadTestConfigFile}}
      resourceGroup: ${{parameters.resourceGroup}}
      loadTestResource: ${{parameters.loadTestResource}}
  - publish: $(System.DefaultWorkingDirectory)/loadTest
    artifact: results

Azure Load Testing の YAML を呼び出す azure-pipelines.yml です。負荷テストの前にビルドやデプロイが必要な場合は、Stage を追加することを想定しています。
トリガーは、スケジュール実行などユースケースに合わせて変更してください。

trigger:
- main

variables:
- name: imageName
  value: 'ubuntu-latest'
- name: azureSubscription
  value: 'azure'
- name: loadTestConfigFile
  value: 'tests/load-test.yml'
- name: resourceGroup
  value: '<Resource Group Name>'
- name: loadTestResource
  value: '<Azure Load Testing Name>'

stages:
- stage: LoadTest
  jobs:
  - template: pipelines/load-test-pipelines.yml
    parameters:
      imageName: $(imageName)
      azureSubscription: $(azureSubscription)
      loadTestConfigFile: $(loadTestConfigFile)
      resourceGroup: $(resourceGroup)
      loadTestResource: $(loadTestResource)

Azure Load Testing の構成ファイルの load-test.yml です。engineInstances が JMeter スクリプトを実行するので、この数を増やすことで負荷テストをスケールアウトできます。1000 ユーザーをシミュレーションしたい場合、4つのエンジンインスタンス(4 x 250 スレッド)を構成することになります。
failureCriteria には、負荷テスト結果の判定基準として総平均応答時間とエラーのパーセンテージを指定しています。

version: v0.1
testName: sample-app-test
testPlan: test-plan.jmx
description: 'Run Sample App Test'
engineInstances: 1
failureCriteria: 
    - avg(response_time_ms) > 1000
    - percentage(error) > 20
env:
  - name: webapp
    value: <Web Apps Name>.azurewebsites.net

JMeter スクリプトの test-plan.jmx です。テスト対象の URL を環境変数 webapp から取得しています。

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.4.1">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <kg.apc.jmeter.threads.UltimateThreadGroup guiclass="kg.apc.jmeter.threads.UltimateThreadGroupGui" testclass="kg.apc.jmeter.threads.UltimateThreadGroup" testname="jp@gc - Ultimate Thread Group" enabled="true">
        <collectionProp name="ultimatethreadgroupdata">
          <collectionProp name="1400604752">
            <stringProp name="1567">5</stringProp>
            <stringProp name="0">0</stringProp>
            <stringProp name="48873">30</stringProp>
            <stringProp name="49710">60</stringProp>
            <stringProp name="10">10</stringProp>
          </collectionProp>
        </collectionProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
      </kg.apc.jmeter.threads.UltimateThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="homepage" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">${__BeanShell( System.getenv("webapp") )}</stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.protocol">https</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path"></stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
          <stringProp name="HTTPSampler.connect_timeout">60000</stringProp>
          <stringProp name="HTTPSampler.response_timeout">60000</stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

実行結果

Azure Pipelines を実行すると負荷テストが行われ、テスト結果を確認できます。

f:id:TonyTonyKun:20211219140607p:plain

テスト結果を artifact に発行しているので、zip ファイルをダウンロードすれば JMeter のツールで可視化することもできます。
将来的には、Azure DevOps でも可視化できるプラグインが提供されると思います。

まとめ

Azure Pipelines で Azure Load Testing のパイプラインを構築して、負荷テストを自動化してみました。
JMeter スクリプトさえ作れば、フルマネージドでお手軽に負荷テストできるのは便利なので、積極的に活用していきたいと思います。
JMeter プラグインが使えるようになれば、さらに用途が広がりそうです。
こちらから、パイプライン YAML の全体を確認できます。
github.com

未解決:Azure Pipelines から環境変数を渡せない

Azure Load Testing の構成ファイルを部品化するために、Azure Pipelines から環境変数を渡したかったのですが、上手く動きませんでした。
docs.microsoft.com

このドキュメントを参考に、下記のように試してみましたが InvalidJSON エラーとなってしまい、未解決です。

f:id:TonyTonyKun:20211219142446p:plain

- task: AzureLoadTest@1
  inputs:
    azureSubscription: 'MyAzureLoadTestingRG'
    loadTestConfigFile: 'SampleApp.yaml'
    loadTestResource: 'MyTest'
    resourceGroup: 'loadtests-rg'
    env: |
      [
          {
              "name": "webapp",
              "value": "myapplication.contoso.com",
          }
      ]

追記:
解決しました。value の行に余計なカンマが付与されていました。
公式ドキュメントのサンプルも修正されています。

- task: AzureLoadTest@1
  inputs:
    azureSubscription: 'MyAzureLoadTestingRG'
    loadTestConfigFile: 'SampleApp.yaml'
    loadTestResource: 'MyTest'
    resourceGroup: 'loadtests-rg'
    env: |
      [
          {
              "name": "webapp",
              "value": "myapplication.contoso.com"
          }
      ]