Azure Pipelines で Azure Load Testing Preview を使ってみる

本記事は [Microsoft Azure Tech | Advent Calendar 2021](https://qiita.com/advent-calendar/2021/microsoft-azure-tech) 12 日目の記事です。

最近プレビューになった [Introducing Azure Load Testing: Optimize app performance at scale | Azure Blog and Updates | Microsoft Azure](https://azure.microsoft.com/en-us/blog/introducing-azure-load-testing-optimize-app-performance-at-scale/) (英語) を、 Azure Pipelines から実行してみました。
普段から Azure Pipelines を使っているユーザーが Azure Load Testing Preview を動かすため必要なことに焦点を当てています。

## Load Testing とは

日本語訳だと負荷テストとも呼ばれています。簡単に説明すると運用環境で想定している指標を使って、期待されるパフォーマンスが問題ないかどうかを確認するためのテストです。
似ているテストだと Stress testing (ストレステスト) というものがあって、こちらは想定以上の負荷をかけたときにどうなるかテストするためのものです。

Microsoft Azure Well-Architected Framework [パフォーマンス テスト - Azure Architecture Center | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/architecture/framework/scalability/performance-test#load-testing) にも掲載されています。

Azure Load Testing の概要はこちらの [What is Azure Load Testing? | Microsoft Docs](https://docs.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing) です。 JMeter 定義ファイルを使うことでいい感じにテストしてくれるよ、というサービスです。

## 事前準備

- Azure Portal から Azure Load Testing Preview のリソースを作成していること
- Azure Pipeline の Service connection (Service Principal) に **Load Test Contributor** ロールがアサインされていること
- Azure DevOps 拡張機能の **Azure Load Testing** がインストールされていること

### Azure Portal から Azure Load Testing Preview のリソースを作成していること

[![Image from Gyazo](https://i.gyazo.com/c13519cf007bd181551955d8453d80f6.png)](https://gyazo.com/c13519cf007bd181551955d8453d80f6)

詳しい手順は [Quickstart: Create and run a load test with Azure Load Testing | Microsoft Docs](https://docs.microsoft.com/en-us/azure/load-testing/quickstart-create-and-run-load-test) に書いてあります。

Azure Portal からテストを実行したり、テスト結果を見るためには **Load Test Contributor or Load Test Owner** のロールアサインが必要です。 Quickstart に書いてあるので、もし Portal から使いたい場合は、そちらも設定を忘れずに。

### Azure Pipeline の Service connection (Service Principal) に **Load Test Contributor** ロールがアサインされていること

```bash
az ad sp show --id "<application-client-id>"

az role assignment create --assignee "<sp-object-id>" \
    --role "Load Test Contributor" \
    --subscription "<subscription-name-or-id>"
```

application-client-id の取得方法などは [Set up Azure Pipelines access permissions for Azure / Tutorial: Identify performance regressions with Azure Load Testing and Azure Pipelines - Azure Load Testing | Microsoft Docs](https://docs.microsoft.com/en-us/azure/load-testing/tutorial-cicd-azure-pipelines#set-up-azure-pipelines-access-permissions-for-azure) に書いてあります。

### Azure DevOps 拡張機能の **Azure Load Testing** がインストールされていること

[![Image from Gyazo](https://i.gyazo.com/843db1436bc7a495765f137624094ebb.png)](https://gyazo.com/843db1436bc7a495765f137624094ebb)

## Pipeline を実行するために必要なファイル

最低限必要なのはこの 3 つのファイルです。ファイルの中身も詳しく見ていきます。

- Azure Pipelines の YAML 定義ファイル
- JMeter の XML 定義ファイル (.jmx)
- Azure Load Testing のテスト構成 YAML 定義ファイル

### Azure Pipelines の YAML 定義ファイル

Azure Load Testing を実行するタスクは AzureLoadTest@1 になっています。
publish はしなくてもよいです。これをすると JMeter のテスト結果ファイル (HTML) ファイルが artifact に保存されます。

```yaml
trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- task: AzureLoadTest@1
  inputs:
    azureSubscription: '<service_connection>'
    loadTestConfigFile: '<load_test_config_file_path>' # Azure Load Testing のテスト構成 YAML 定義ファイル
    resourceGroup: '<resource_group>'
    loadTestResource: '<load_test_resource>' # Azure Portal から作った Azure Load Testing Preview リソース名

- publish: $(System.DefaultWorkingDirectory)/loadTest
  artifact: results
```

### JMeter の XML 定義ファイル (.jmx)

Quickstart から持ってきたものと同じです。 {your-endpoint-url} は、テストしたいエンドポイントに置き換えてください。 Quickstart の注意書きにもありますが、 エンドポイントに https または http を含めると動きません。末尾にスラッシュをつけるのもダメだったような気がします。
参考までに今回設定した値は `web1aa.azurewebsites.net` です。

```xml
<?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">{your-endpoint-url}</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 Load Testing のテスト構成 YAML 定義ファイル

Azure Pipelines チュートリアル Docs で紹介されている GitHub の [nodejs-appsvc-cosmosdb-bottleneck/SampleApp.yaml at main · Azure-Samples/nodejs-appsvc-cosmosdb-bottleneck](https://github.com/Azure-Samples/nodejs-appsvc-cosmosdb-bottleneck/blob/main/SampleApp.yaml) から持ってきました。 Docs について詳しくは補足、おまけで解説します。

```yaml
version: v0.1
testName: <any>
testPlan: <jmeter_xml_path> # JMeter の XML 定義ファイル (.jmx)
description: '<any>'
engineInstances: 1
```

## 実行

こんな感じの実行結果になります。
[![Image from Gyazo](https://i.gyazo.com/8f4ade46c64365ceefb62656bd394d04.png)](https://gyazo.com/8f4ade46c64365ceefb62656bd394d04)

Azure Portal から見ると感じで見えます。（訳合って別のテスト結果を見せています。）
[![Image from Gyazo](https://i.gyazo.com/cf534d38cf0f8e8457e5a354383e77a3.png)](https://gyazo.com/cf534d38cf0f8e8457e5a354383e77a3)

Publish した artifact はこんな感じの HTML を出力します。
[![Image from Gyazo](https://i.gyazo.com/d9e214d35821a403a205f3f75c391a7a.png)](https://gyazo.com/d9e214d35821a403a205f3f75c391a7a)

### Failure criteria を設定して、テストを失敗させてみる

せっかくなので、 Azure Load Testing のテスト構成 YAML 定義ファイルに Failure criteria を設定して、テストを失敗させてみます。
今回のエラー条件は応答時間の平均が 10 ms より大きいか、エラー (ステータスコードが 2xx 系じゃない場合？) の割合が 20 % より大きいときに発生するように設定しています。

```yaml
version: v0.1
testName: <any>
testPlan: <jmeter_xml_path> # JMeter の XML 定義ファイル (.jmx)
description: '<any>'
engineInstances: 1
failureCriteria: 
    - avg(response_time_ms) > 10
    - percentage(error) > 20
```

実行した結果、応答時間が 15.96 ms だったのでテストが失敗しています。 Pipeline ジョブも Failed になっています。

[![Image from Gyazo](https://i.gyazo.com/b638363f72d1454ecca0fa34d9a8f836.png)](https://gyazo.com/b638363f72d1454ecca0fa34d9a8f836)

## 補足、おまけ

触っていて気になったことや、トラブルシューティングをまとめました。プレビューなので、そういうものだと思って使ってみると楽しいと思います。

### Azure Load Testing の Docs に Azure Pipelines を使ったチュートリアルがある

Docs: [Tutorial: Identify performance regressions with Azure Load Testing and Azure Pipelines - Azure Load Testing | Microsoft Docs](https://docs.microsoft.com/en-us/azure/load-testing/tutorial-cicd-azure-pipelines)

こちらのチュートリアルだとテスト対象の App Service などのリソースも作成します。もちろん App Service の作成にはコストがかかりますので、色々と許容できる人向けです。
実際に Pipeline を実行したところ、いくつかエラーとなったハマりポイントがあったので、解決策とともに記載しておきます。

> Failed to create the resource group. Error: xxx

azure-pipelines.yml の `resourceGroupName: '$(webAppName)-rg'` を `resourceGroupName: '$(loadTestResourceGroup)'` に変更することで解決します。

これは Azure DevOps から自動作成した Service connection に、別のリソースグループの作成権限がないからです。
Service connection を自動作成するときに大抵は Load Testing 用のリソースグループ `$(loadTestResourceGroup)` を選択すると思うのですが、それだと `$(webAppName)-rg` が作成できず、このエラーが発生します。
よって、一番楽な解決策としては Web App を作成するリソースグループの設定を、 Load testing と同じリソースグループにしてしまうことです。リソースの削除も一括でできるので一石二鳥です。

> [error]MissingSubscriptionRegistration: The subscription is not registered to use namespace 'Microsoft.DocumentDB'.

Azure Portal から Subscription を開いて、Resource providers に Microsoft.DocumentDB を登録することで解決します。
[![Image from Gyazo](https://i.gyazo.com/7bc998555bb691438912c955c9833f51.png)](https://gyazo.com/7bc998555bb691438912c955c9833f51)

僕の環境では NotRegistered だったので発生していました。

> [error]Error: Package deployment using ZIP Deploy failed. Refer logs for more details.

リソースグループを空の状態 (or Load Testing リソースのみの状態) にして実行したら直りました。何度か途中で失敗したので、リソースの状態が変になってしまったのかもしれません。

### テスト結果が HTML ではなく testreport.csv になる

テストを実行しなおすか failureCriteria を書くと直りました。タイミングの問題もあるかもしれません。また、これが発生すると Azure Portal 上でも結果が表示されなくなります。プレビューらしいですね。僕は好きです。

### Test Plans にも Load test がいる

Azure DevOps には Test Plans という機能があって、ここにも Load test がいます。しかし、この Load test はもうなくなりましたので、間違えないようにしましょう。
[![Image from Gyazo](https://i.gyazo.com/eb445227d5a6189f7b70202a4e24d86a.png)](https://gyazo.com/eb445227d5a6189f7b70202a4e24d86a)

### JMeter の XML 定義ファイル (.jmx) 書くのがつらい

JMeter をインストールして、 JMeter GUI (アプリ) を使って書くのがよいのかもしれません。他にも何かよい書き方があったら教えてください。