パブリックプレビューの Azure Load Testing を試してみた

## 背景と目的

負荷テストと聞いて思い当たるのは、リリース判定前にシステム設計で想定した性能値の範囲内かどうかを確認したり、オートスケール構成で負荷をかけて実際にオートスケールするかを確認したり、というのが今までの使い方でした。マネージドでしかも CI/CD パイプラインに組み込んで負荷テストができるとなると、アプリの機能追加によってシステム全体の性能が悪化するなんて事も簡単に計測できるようになりそうですね。という事で、今後楽しみなサービスなので実際に試してみました。

## 前提条件

コマンドの実施環境は、Mac + Azure CLI です。

```bash
$ sw_vers
ProductName:    macOS
ProductVersion: 12.0.1
BuildVersion:   21A559

$ az version
{
  "azure-cli": "2.30.0",
  "azure-cli-core": "2.30.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
```

## LoadTestService を登録

```bash
# リソースプロバイダーの LoadTestService を登録します
az provider register \
  --namespace Microsoft.LoadTestService

# Registered になったのを確認します
az provider show \
  --namespace Microsoft.LoadTestService \
  --query registrationState \
  --output tsv

# Load Test が使用できるリージョンを調べます（私の環境では以下のリージョンでした）
az provider show \
  --namespace Microsoft.LoadTestService \
  --query "resourceTypes[?resourceType=='loadtests'].locations[]" \
  --output tsv

East US
North Europe
West US 2
Southeast Asia
Australia East
East US 2
South Central US
```

## Load Test を作成

```bash
# 環境変数をセットします
region=southeastasia
prefix=mnrloadtest
sid=$(az account show --query id --output tsv)

# リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

# Load Test を作成します
az rest \
  --method put \
  --url "https://management.azure.com/subscriptions/$sid/resourceGroups/${prefix}-rg/providers/Microsoft.LoadTestService/loadtests/${prefix}?api-version=2021-12-01-preview" \
  --body "{\"location\": \"$region\"}"

# 作成した Load Test に対して自分自身を Load Test Contributor ロールに割り当てます
az role assignment create \
  --assignee $(az account show \
  --query "user.name" \
  --output tsv) \
  --scope "/subscriptions/$sid/resourceGroups/${prefix}-rg/providers/Microsoft.LoadTestService/loadtests/${prefix}" \
  --role "Load Test Contributor"
```

Azure ポータルの Azure Load Testing リソース内で、下記のようなメッセージが表示され続ける場合は、一度ブラウザを終了して開き直すと消えていました。

> You are not authorized to use this resource, and need to be assigned the Load Test Owner, Load Test Contributor, or Load Test Reader role. The person assigning Azure roles needs to have Microsoft.Authorization/roleAssignments/write permissions, such as User Access Administrator or Owner. In case role was recently granted, please refresh the page and try again.


## 検証用 VM を作成

```bash
# 負荷をかける検証用 VM を作成します（ログインしないのでパスワードは何でも良いです）
az vm create \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm \
  --os-disk-name ${prefix}-vmOSDisk \
  --image CentOS \
  --admin-username azureuser \
  --admin-password $(openssl rand -base64 16) \
  --size Standard_A1_v2 \
  --nsg-rule NONE \
  --storage-sku Standard_LRS

# Apache をインストールします
az vm run-command invoke \
  --command-id RunShellScript \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm \
  --query "value[0].message" \
  --output tsv \
  --scripts "yum -y install httpd && systemctl start httpd && systemctl enable httpd"

# パブリック IP アドレスから HTTP 接続出来るようにします
az network nsg rule create \
  --resource-group ${prefix}-rg \
  --name Allow-HTTP \
  --nsg-name $(az network nsg list \
  --resource-group ${prefix}-rg \
  --query "[].name" \
  --out tsv) \
  --priority 100 \
  --source-address-prefixes "*" \
  --destination-port-ranges 80 \
  --access Allow \
  --protocol Tcp

# パブリック IP アドレスを取得します
pip=$(az vm show \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm \
  --show-detail \
  --query publicIps \
  --output tsv)
echo $pip

# HTTP アクセスして確認します
curl http://$pip
```

## 負荷テストスクリプトを作成

Apache JMeter スクリプトを作成します。HTTPSampler.domain に検証用 VM のパブリック IP アドレスを入れ、HTTPSampler.protocol は http にします。

```bash
cat <<EOF > ${prefix}-sampletest.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>
      <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">$pip</stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.protocol">http</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>
EOF
```

## Azure ポータルでサンプルテストを作成

こちらの手順に従い、サンプルテストを作成しました。

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

## サンプルテストの結果

```bash
# Apache のアクセスログを確認します
az vm run-command invoke \
  --command-id RunShellScript \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm \
  --query "value[0].message" \
  --output tsv \
  --scripts "tail /var/log/httpd/access_log"

Enable succeeded: 
[stdout]
20.198.153.60 - - [05/Dec/2021:01:28:03 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:03 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
20.198.153.60 - - [05/Dec/2021:01:28:04 +0000] "GET / HTTP/1.1" 403 4897 "-" "Apache-HttpClient/4.5.12 (Java/15.0.5)"
```

![azure-load-test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675885/16487fc4-c5ba-0011-8b38-53d68aa8900c.png)

## 参考

作成したリソースを削除します。

```bash
# リソースグループを削除します
az group delete \
  --name ${prefix}-rg
```

参考サイトです。

https://azure.microsoft.com/ja-jp/updates/azure-load-testing-now-available-in-public-preview/

https://azure.microsoft.com/ja-jp/blog/introducing-azure-load-testing-optimize-app-performance-at-scale/

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