ROMANCE DAWN for the new world

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

Azure Container Instances を使って Cypress で負荷テストを実行する

Web アプリケーションの負荷テストを実行したくなったので、Azure Container Instances(ACI)を使って、Cypress を実行する仕組みを作ってみました。

Cypress とは

Cypress は、Web アプリケーションの E2E テストを実行するためのツールです。Selenium よりも、シンプルに導入でき、テスト実行も速いです。
npm でインストールでき、Test Runner は無料で使えるので、CI パイプラインにも組み込みやすいです。
SaaS で提供されている Dashboard を連携させると、テスト結果のエビデンス(スクリーンショットや動画のファイル)が自動でアップロードされます。3ユーザーで月500テストケースまでなら無料です。
www.cypress.io

Cypress でテストケースを作成する

例えば、Google で「cypress.io」を検索して結果ページのタイトルを検証するテストケースであれば、こんな感じで書きます。

const targetUrl = Cypress.env('target_url')

describe('My First Test', function() {
  it('Visit Google', function() {
    cy.visit(targetUrl)

    cy.get("input[name='q']")
      .type('cypress.io')

    cy.contains('Google 検索')
      .click()

    cy.title().should('eq', 'cypress.io - Google 検索')
  })
})

Cypress.env で、環境変数からテスト対象の URL を取得しています。初期値は、cypress.env.json で指定しています。

{
    "target_url": "https://www.google.co.jp"
}

npx cypress run コマンドでテストを実行します。

f:id:TonyTonyKun:20201018113100p:plain

UI を使ってインタラクティブに実行させたい場合は、npx cypress open コマンドを使います。

Cypress を実行する Docker Images を作成する

Cypress を ACI で動かしたいので、Docker Images を作ります。Cypress の公式イメージを使って、日本語フォントを追加しておきました。

FROM cypress/included:5.3.0

# 日本語フォントを追加
RUN apt-get install --no-install-recommends -y fonts-noto fonts-noto-cjk

WORKDIR /app

COPY ./cypress ./cypress
COPY ./cypress.json ./cypress.json
COPY ./cypress.env.json ./cypress.env.json

RUN npx cypress run

これで Cypress を Docker で実行できるようになりました。

$ docker build -t thara0402/cypress-docker:1.0.0 .
$ docker run --rm -it thara0402/cypress-docker:1.0.0

今回は、Docker Hub に Images をプッシュしておきます。

$ docker push thara0402/cypress-docker:1.0.0

Azure Container Instances で負荷テストを実行する

ACI は、ARM Template を使ってデプロイします。

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resourceNamePrefix": {
            "type": "string"
        },
        "imageType": {
            "type": "string",
            "allowedValues": [
                "Public",
                "Private"
            ]
        },
        "imageName": {
            "type": "string"
        },
        "osType": {
            "type": "string",
            "allowedValues": [
                "Linux",
                "Windows"
            ]
        },
        "numberCpuCores": {
            "type": "string"
        },
        "memory": {
            "type": "string"
        },
        "restartPolicy": {
            "type": "string",
            "allowedValues": [
                "OnFailure",
                "Always",
                "Never"
            ]
        },
        "ipAddressType": {
            "type": "string"
        },
        "ports": {
            "type": "array"
        },
        "environmentVariables": {
            "type": "array"
        },
        "numberOfInstances": {
            "type": "int",
            "minValue": 1,
            "maxValue": 10
        }
    },
    "variables": {
        "containerInstanceName":"[concat(parameters('resourceNamePrefix'), '-aci')]"
    },
    "resources": [
        {
            "location": "[resourceGroup().location]",
            "name": "[concat(variables('containerInstanceName'), copyindex())]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2019-12-01",
            "properties": {
                "containers": [
                    {
                        "name": "[concat(variables('containerInstanceName'), copyindex())]",
                        "properties": {
                            "image": "[parameters('imageName')]",
                            "resources": {
                                "requests": {
                                    "cpu": "[int(parameters('numberCpuCores'))]",
                                    "memoryInGB": "[float(parameters('memory'))]"
                                }
                            },
                            "environmentVariables": "[parameters('environmentVariables')]",
                            "ports": "[parameters('ports')]"
                        }
                    }
                ],
                "restartPolicy": "[parameters('restartPolicy')]",
                "osType": "[parameters('osType')]",
                "ipAddress": {
                    "type": "[parameters('ipAddressType')]",
                    "ports": "[parameters('ports')]",
                    "dnsNameLabel": "[concat(variables('containerInstanceName'), copyindex())]"
                }
            },
            "tags": {},
            "copy": {
                "name": "containerInstanceLoop",
                "count": "[parameters('numberOfInstances')]"
            }
       }
    ],
    "outputs": {
    }
}

テストが失敗した場合に ACI が再起動されないように、restartPolicyNever を指定しています。環境変数は environmentVariables で指定していて、環境変数名のプレフィックスに cypress_ が必要です。

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resourceNamePrefix": {
            "value": "<ACIの名前のプレフィックスを指定してください>"
        },
        "imageType": {
            "value": "Public"
        },
        "imageName": {
            "value": "thara0402/cypress-docker:1.3.0"
        },
        "osType": {
            "value": "Linux"
        },
        "numberCpuCores": {
            "value": "1"
        },
        "memory": {
            "value": "1.5"
        },
        "restartPolicy": {
            "value": "Never"
        },
        "ipAddressType": {
            "value": "Public"
        },
        "ports": {
            "value": [
                {
                    "port": "80",
                    "protocol": "TCP"
                }
            ]
        },
        "environmentVariables": {
            "value": [
                {
                    "name": "cypress_target_url",
                    "value": "https://www.google.co.jp"
                }
            ]
        },
        "numberOfInstances": {
            "value": 1
        }
    }
}

numberOfInstances に指定した数で、ACI を使った Cypress の負荷テストを実行できます。

$ az group create -n <ResourceGroup Name> -l japaneast
$ az group deployment create -g <ResourceGroup Name> --template-file template.json --parameters parameters.json

繰り返し負荷テストを実行する場合は、ACI を開始するスクリプトを組めば OK です。

#!/usr/bin/env bash
for aci in $(az container list --resource-group <ResourceGroup Name> --query "[].name" -o tsv); do
    az container start -g <ResourceGroup Name> -n $aci --no-wait
    echo $aci starting...
done

負荷テスト結果を確認する

負荷テスト結果は、ACI のステータスを確認して判断することにします。
Azure CLI の az container list で ACI のリストを取得し、az container show でステータスを取得します。

$ az container list --resource-group <ResourceGroup Name> --query "[].id" -o tsv | xargs -I {} -P 10 az container show --query "{name:name,state:instanceView.state}" -o tsv --ids "{}"
test-runner-aci0    Succeeded
test-runner-aci1    Succeeded
test-runner-aci2    Succeeded

まとめ

Azure Container Instances を使って Cypress で負荷テストを実行してみました。
テスト結果を残したい場合は、Cypress Dashboard と連携させたり、AzCopy で Blob にファイルを転送したりする構成を追加する必要があります。
Docker で動かせるのであれば、Cypress でなくとも使える仕組みです。このようなユースケースは、ACI が向いていると思います。

Azure サブスクリプションでリージョン毎にデプロイできる ACI はクオーターが100個までなので、もっと大規模な負荷テストを実行する場合は、Azure Kubernetes Services を使ったほうがよさそうです。
今回のスクリプト一式は、こちらで公開しています。
github.com
github.com