初学者がlocustをAWS ECS(Fargate)で実行する

https://qiita.com/sekikatsu/items/992e82671aa505c5a652 の続きです。

ローカル環境でlocustを実行できるようになったので、クラウドの豊富なマシンパワーを使って負荷掛けができるようにします。
Terraformを使って本格的にやる場合は https://qiita.com/neilli-sable/items/b17dfba5eabfcafccaf8 など既存記事があるので、ご参考ください。
この記事は、ECSを初めて使うくらいの人が、とりあえずAWS Webコンソールを使ってlocustを動かしてみる、という内容です。

# 序章

「ローカルで動いてるんだからAWSに移行するのも簡単やろ」と思ってたら、全然簡単じゃなかった。ECSのハードル高い。

とりあえず、スタート時点での知識レベルは以下くらい

- ECS: AWSでコンテナ使うためのサービス。EKSとかいう玄人向けサービスは無視。
    - 「ECSって？」という状態なら https://xn--o9j8h1c9hb5756dt0ua226amc1a.com/?p=2025 が参考になった
- Fargate: ECSを使うためのインフラ。FargateかEC2を選べるが、IaaSよりPaaSの方が楽に違いないとこちらを選択。
- locust: https://qiita.com/sekikatsu/items/992e82671aa505c5a652 これ書いた。

# 流れ

以下のフローで実行できるようにしようとした

1. master 1台 + slave N台を起動する
1. コンテナが起動すると、指定したURLからlocustfile.pyをgit clone
1. cloneしたlocustfile.pyを使ってlocustをmaster/slaveで起動
1. WebUIから負荷掛け
1. WebUIから結果をダウンロード。locustの場合、結果の回収方法を考えなくていいので楽（JmeterだとS3に転送したりしないといけない）

# 方針

- locustio/locustイメージはgitが入っていないので、自作する
- Dockerイメージ内にmaster/slaveそれぞれ用のスクリプトを用意して、それを叩くことでmaster/slaveの切り替えと複数コマンド実行を実現
  - masterとslaveでDockerイメージを分けるのは面倒だったので、横着して一つにまとめる
  - ECSのCMDで複数コマンドを実行する方法がわからなかったので、スクリプトを置いてコマンドを一つにする
      - 今回はここに一番時間を取られた
      - CMDに`/bin/bash,-c,'git clone ** && locust **'`を設定すると、`No such file or directory`と心ないことを言われる
      - CMDに`/bin/bash,-c,git,clone,(略)`を設定しても、動かず、かつログも出力されず、心が折れた
  - 今思えば、イメージ内に入れるのではなく、これもGitHubに入れてgit cloneで取得すれば良かった
- 本当はMasterへの接続先を自動で取ってきてSlaveを起動したかったんだけど、別のところでハマりすぎて心が折れたので、目で確認＋手で入力、という最強の手段を用いる
- 変数は全て環境変数経由で渡す

# Dockerイメージを用意する

使う環境変数

- LOCUST_FILE_GIT_URL: locustfile.pyを取得するための git clone URL
- LOCUST_TARGET_URL: 負荷掛け対象のURL。あとでWebから上書きできるので、適当でもいい
- LOCUST_MASTER: slave用。Masterのprivate IPアドレスを入れる。


```:ディレクトリ構造
- カレントディレクトリ
  - Dockerfile
  - master
  - slave
```

master用のエントリポイント。
Slaveの起動時に必要なMasterのIPアドレスを一応出力しておく（実際にはAWSコンソール上で見る方が早いから使わないけど）。

```sh:master
#!/bin/bash
echo "=== IP Address ===================="
ip address show
echo "=== Git clone ====================="
git clone ${LOCUST_FILE_GIT_URL} /locust_src
echo "=== Start LOCUST as master mode ==="
locust -f /locust_src/locustfile.py --master -H ${LOCUST_TARGET_URL}
```

slave用のエントリポイント

```sh:slave
#!/bin/bash
echo "=== Confirm to connect master ===="
ping -c 3 ${LOCUST_MASTER}
echo "=== Git clone ===================="
git clone ${LOCUST_FILE_GIT_URL} /locust_src
echo "=== Start LOCUST as slave mode ==="
locust -f /locust_src/locustfile.py --worker --master-host ${LOCUST_MASTER}
```

Dockerfile

```docker:Dockerfile
FROM python:3.6

RUN pip install locust

RUN mkdir /locust
ADD ./master /locust/master
ADD ./slave /locust/slave
RUN chmod +x /locust/master
RUN chmod +x /locust/slave

CMD ["/locust/master"]
```

この状態でdocker build + push。

```sh
docker build -t sekikatsu36/locust:0.0.1 .
docker push sekikatsu36/locust:0.0.1
```

できたのがこれ
https://hub.docker.com/repository/docker/sekikatsu36/locust/general

# locustfile.pyの用意

githubリポジトリに作ってpushするだけ。
今回はPOSTを使った負荷掛けをしたかったので、Bodyに入れるパラメータも環境変数から取るようにした。

使う環境変数

- LOCUST_TARGET_PATH: 負荷掛けの対象パス。上の`LOCUST_TARGET_URL`と組み合わせることでフルパスになる。
- LOCUST_PARAM: パラメータを`key1=value;key2=value2`の形式で入れる

```py:locustfile.py
from locust import HttpUser, TaskSet, task, between, constant
import os

class UserBehavior(TaskSet):
    @task(1)
    def login(self):

        l = os.environ['LOCUST_PARAM'].split(';')
        tmp = map(lambda s:s.split('='), l)
        param = dict(tmp)

        self.client.post(os.environ['LOCUST_TARGET_PATH'], param)

class WebsiteUser(HttpUser):
    tasks = {UserBehavior:1}
    wait_time = constant(1)
```

できたのがこれ
https://github.com/sekikatsu36/locust-py

# AWSでECSクラスタを作る

やっとAWSの出番。

- クラスターテンプレートは「ネットワーキングのみ AWS Fargateを使用」を選択。
    - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/18e99062-1191-306d-e6d2-f1e99ddf53d5.png">
- クラスタ名は適当につける
- VPCはお好みで。
- 負荷掛けにしか使わないので、Container Insightsは要らない
    - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/26f287ee-ee43-c609-8d75-7b3e82509959.png">

# タスク定義を作る

どんなタスク（コンテナ）を実行するのか、定義する。
あくまでテンプレート的なもので、タスクの実行時に定義は上書きできるので、横着してタスク定義もmaster/slaveで使い回す。

- 「起動タイプの互換性の選択」は`FARGATE`で
    - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/ab622ee8-ed0a-0050-76e4-cf62cd72bfea.png">
- タスクサイズは、実行する処理によるけど、試しに動かすなら`メモリ=1GB、vCPU=0.5`で十分
    - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/6fbaafe8-dd8f-4325-7fd5-5b1ba0c5b7e3.png">

- 「コンテナの追加」ボタンから、作ったDockerイメージを登録する
    - コンテナ名は適当に
    - イメージはさっき作ったやつ。今回は`sekikatsu36/locust:0.0.1`
    - ポートマッピングは 8089(WebUI用)と5557/5558(Slave=>Master通信)を開ける
        - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/0f5dbeb1-f6f1-4d83-5640-cbdf04f298f6.png">
    - 環境はコマンドだけ`/locust/master`を入れておく。なくてもデフォルトこれなので動くが、あとでSlaveを変更するときに入ってると個人的に変更しやすかった
    - 環境変数は必要な値を適宜設定。LOCUST_MASTERはあとで毎回上書きする必要があるので、適当に入れる。
        - <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/2a0ecb50-df60-1ec1-540a-64c96950496f.png">
    - ログ設定は特にいじらなければ、CloudWatch Logsの`/ecs/{コンテナ名}`に出力されるようになる。今回はそのまま。
- 記載のないものはデフォルトで

# セキュリティグループを作る

master/slaveに適用されるセキュリティグループを作る。
この操作自体はタスクの実行時にもできそうなUIをしているが、そちらはUIがダメダメすぎて使えないので、事前に作っておく。（このUIで無駄にてこずってしまった）

- まず、ルールを何も買えない状態でセキュリティグループを作る。そうするとインバウンドルールが空になっているはず。
- そのあとで、以下のインバウンドルールを追加する
    - WebUIにつなぐために、手元から8089ポートへの通信を開ける
    - Slave=>Masterの通信を許可するために、「ソース」にこのセキュリティグループ自体、「ポート範囲」に`5557-5558`を指定。
- <img width="400" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/6d79b094-5004-2ccf-ca20-c2ffb108062d.png">

# masterを実行

- 作ったクラスターの「タスク」タブから、「新しいタスクの実行」を選ぶ
- 「キャパシティープロバイダーの戦略」 でエラーが出る。が、今は使わないので「起動タイプへ切り替える」を選択して、FARGATEを選ぶ
- VPCとサブネットを適当に選択
- セキュリティグループはさっき作ったやつ
- 記載のないものはデフォルトで

実行できたら、タスクのプライベートIPを確認する。
http://(プライベートIP):8089 にアクセスすると、locustの画面が出てくるはず

# slaveを実行

masterと同じ手順でタスクを実行。違いは以下。

- コンテナを以下のように上書き
    - コマンドを `/locust/slave`
    - 環境変数の`LOCUST_MASTER`を、MasterのプライベートIPアドレスに変更
- セキュリティグループは同じのも使いまわせるが、8089も5557/5558も開ける必要はないので、気になるなら閉じたものを作成＆設定
- タスクの数を用途に合わせて適宜変更
- サブネットは複数指定できるので、Slaveのサブネットを分散させたい場合、配置予定のサブネットを全て登録

slaveが立ち上がったあと、locustのWebUIでWorkerの数が増えていれば成功。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/4d1e58b3-6729-9e3a-2ed1-c06f745a8cda.png)

あとはローカルと同じように負荷掛けを実行するだけ

# おまけ

masterとslaveの実行が若干面倒くさいので、CLIで叩きたいという欲が出る。

## masterのタスク実行をCLIで行う

masterはタスク定義をそのまま実行するだけなので、簡単。

```sh
aws ecs run-task \
 --cluster locust-test \
 --count 1 \
 --launch-type FARGATE \
 --task-definition locust-base \
 --network-configuration "awsvpcConfiguration={subnets=['サブネット1','サブネット2',(略)],securityGroups=['セキュリティグループID'],assignPublicIp=ENABLED}"
```

- サブネットは`subnet-***`というサブネットIDをひたすら列挙するだけ
- セキュリティグループIDも`sg-***`を設定する

蛇足だけど、AWS WebコンソールでこのID群を確認するのが面倒臭いのは私だけだろうか。UIの改善を求めたい。

## slaveのタスク実行をCLIで行う。

slaveはコンテナの設定を上書きする都合で、ちょっと面倒くさい。
コンテナの設定値は差分をJsonで表現して与える必要がある。

```sh
$ #差分のJsonファイルの雛形を作る
$ echo "{\"containerOverrides\":[{\"name\":\"locust\",\"command\":[\"/locust/slave\"],\"environment\":[{\"name\":\"LOCUST_MASTER\",\"value\":\"MASTER_IP\"}]}]}" > override-for-slave-base.json 
$ cat override-for-slave-base.json | jq
{
  "containerOverrides": [
    {
      "name": "locust",
      "command": [
        "/locust/slave"
      ],
      "environment": [
        {
          "name": "LOCUST_MASTER",
          "value": "MASTER_IP"
        }
      ]
    }
  ]
}
$ #masterのIPアドレスを置換
$ sed "s/MASTER_IP/確認したマスターのIPアドレス/g" override-for-slave-base.json > override-for-slave.json
$ #slaveを立ち上げる
$ aws ecs run-task \
 --cluster locust-test \
 --count 10 \ #slaveの必要個数を入力
 --launch-type FARGATE \
 --task-definition locust-base \
 --network-configuration "awsvpcConfiguration={subnets=['サブネット1','サブネット2',(略)],securityGroups=['セキュリティグループID'],assignPublicIp=DISABLED}" \
 --overrides file://./override-for-slave.json
```

terraformで一発、とまではいかないけれど、まぁまぁ楽に起動できるようにはなった。