Azure Load Testingを使って簡単な負荷テストをCI/CDに組み込む

表題の通り、Azure Load Testingを使って簡単な負荷テストをGithubActionで自動化するといった内容をやってみた際の備忘録です。
ほぼ、こちらのドキュメントをなぞっている形になります🙏

https://learn.microsoft.com/ja-jp/azure/load-testing/tutorial-identify-performance-regression-with-cicd?tabs=pipelines

## テスト用のサンプルサイトを作成
Next.jsでサンプルサイトは作りました。GETリクエストが合った度に適当なデータをCosmosDBへ保存し、格納されているデータの合計数を表示する。といった簡単なものをWebAppにGithub Actionsでデプロイできるようにしておきます。
こんなサイトをテスト用に用意👇
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/39d55374-486a-08ee-b618-b189c34ef8d7.png)
ターゲットとなるアプリはなんでもいいので、こちらについては省略します。

## サービスプリンシパルの作成
それでは本題。まずはGithub ActionsのワークフローからAzureにアクセスできるように共同作成者ロールを割り当てる作業です。
AzureにログインしてCloud Shell を立ち上げます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/4e42c873-7721-2604-4d32-91e78d03e4d2.png)

以下のコマンドを入力します。
```bash:bash
subscription=$(az account show --query "id" -o tsv)
echo $subscription
```
```bash:bash
az ad sp create-for-rbac --name "my-load-test-cicd" --role contributor \
                         --scopes /subscriptions/$subscription \
                         --sdk-auth
```
するとサービスプリンシパルを表すJSONが出力されます、
これをコピーしてメモっておきます。
```
{
  "clientId": "00000000-0000-0000-0000-000000000000",
  "clientSecret": "00000000-0000-0000-0000-000000000000",
  "subscriptionId": "00000000-0000-0000-0000-000000000000",
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"    
}
```
AzureADで見ると追加されている事が分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/c3830f0e-263a-28ca-7f09-f18a9cce5bb8.png)

続いて、サービスプリンシパルに Load Test Contributor ロールを割り当てます。
```bash:bash
object_id=$(az ad sp list --filter "displayname eq 'my-load-test-cicd'" --query "[0].id" -o tsv)

az role assignment create --assignee $object_id --role "Load Test Contributor" --scope /subscriptions/$subscription --subscription $subscription
```

## Azure Load Testing
AzureポータルからLoad Testingをデプロイしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/6f1efe1f-2f60-3e41-6047-729df6a6177c.png)

https://learn.microsoft.com/ja-jp/azure/load-testing/overview-what-is-azure-load-testing

https://learn.microsoft.com/ja-jp/azure/load-testing/tutorial-identify-bottlenecks-azure-portal

今回、テスト用のスクリプトはJMeterで簡単なものを作成しました。
```xml:quick_test.jmx
<?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>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">500</stringProp>
        <stringProp name="ThreadGroup.ramp_time">0</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">100</stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" 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">******.azurewebsites.net</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.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

```

- **ThreadGroup.main_controller**:
    - ループコントローラーがどのように動作するかを制御します。ここでは、"continue_forever"が"false"、"loops"が"1"なので、テストは一回だけ実行されます。
- **ThreadGroup.num_threads**:
    - このパラメータはスレッドの数（つまり仮想ユーザーの数）を指定します。ここでは500と設定したので、500の仮想ユーザーがテストを行います。
- **ThreadGroup.ramp_time**:
    - これは全てのユーザーが起動するまでの時間（秒）を指定します。ここでは0と設定したので、すべてのユーザーがすぐに起動します。
- **ThreadGroup.scheduler**:
    - スケジューラを使用するかどうかを制御します。ここでは"true"と設定したので、スケジューラが使用されます。
- **ThreadGroup.duration**:
    - スケジューラが有効な場合、このパラメータはテストの実行時間（秒）を指定します。ここでは100と設定したので、テストは100秒間実行されます。
- **HTTPSampler.domain**:
    - これはリクエストが送信されるドメイン（またはIPアドレス）を指定します。
- **HTTPSampler.port**:
    - これは接続するポート番号を指定します。ここでは空欄です。
- **HTTPSampler.protocol**:
    - これは使用するプロトコルを指定します。ここでは "https" を指定しています。
- **HTTPSampler.path**:
    - これはリクエストのパスを指定します。
- **HTTPSampler.method**:
    - これは使用するHTTPメソッド（GET、POST、PUTなど）を指定します。ここでは "GET" を指定してます。

設定ファイルはこちらを用意。
```quick_test.yaml
displayName: Get_load-test-apps-001.azurewebsites.net
testPlan: quick_test.jmx
description: 
engineInstances: 3 #エンジンインタンスの数
testId: 5886d1f4-2033-45c5-b80c-c40f3df317a5
```

## Github
Githubレポジトリのシークレットに先程のJSONで出力されたサービスプリンシパルの情報を設定しておきます。
名前は`AZURE_CREDENTIALS`として、値には先程のJSONを貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/aa62348d-ea42-1bcd-d9d2-5f5dd6cfa9fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/c40efe86-fcd1-a0cc-5ee6-f2605d2afd32.png)

## デプロイしてテスト実行
`quick_test.jmx`  と `quick_test.yaml`をワークディレクトリの直下に保存します。
`.github/workflow/`内の`yaml`ファイルを編集します。
以下を追記しました。
```yaml:yaml
  loadTest:
    name: Load Test
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitHub Actions 
        uses: actions/checkout@v2
          
      - name: Login to Azure
        uses: azure/login@v1
        continue-on-error: false
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
          
      - name: 'Azure Load Testing'
        uses: azure/load-testing@v1
        with:
          loadTestConfigFile: 'quick_test.yaml' #configFileName
          loadTestResource: 'my-load-test-test' #ResourceName
          resourceGroup: 'my-load-test-rg' #ResourceGroupName
              
      - uses: actions/upload-artifact@v2
        with:
          name: loadTestResults
          path: ${{ github.workspace }}/loadTest
```
https://github.com/marketplace/actions/azure-load-testing?version=v1.1.15

変更が終わったら、Pushします。
無事デプロイからテストが実行されたようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/28f65d4a-7f86-6c0e-e4b1-8978d5e1b669.png)

ここからテスト結果をcsv形式でダウンロードもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/a2bcbac6-8b78-4175-b698-9c1e6bdd49fb.png)

因みに、こういった条件を追加する事でテストに不合格の条件を追加できます。
```diff_yaml:quick_test.yaml
displayName: Get_load-test-apps-001.azurewebsites.net
testPlan: quick_test.jmx
description: 
engineInstances: 3
testId: 5886d1f4-2033-45c5-b80c-c40f3df317a5

+ failureCriteria: 
+  - percentage(error) > 50
```
エラー割合が50%以上なので不合格になりました。
ぽんこつアプリが爆誕しました…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/5635363c-a6a3-ae0e-8bea-095c079433a3.png)

## さいごに
今回、CI/CDに組み込むという部分だけ抜粋しましたが、公式のチュートリアルではここからボトルネックを特定して改善するといった一連の流れも記載されています。
負荷テスト初心者なので、JMeterの使い方含めて継続して学習していこうと思います。

:::note warn
Azureに作成したリソースは使い終わったら削除する事を忘れずに！
:::

### 参考

https://learn.microsoft.com/ja-jp/azure/load-testing/tutorial-identify-performance-regression-with-cicd?tabs=github

https://learn.microsoft.com/ja-jp/azure/load-testing/quickstart-create-and-run-load-test

https://qiita.com/georgeOsdDev@github/items/f6757afc46b6fc5566bb