LocustをEKS上で動かす

# この記事について

負荷試験にてLocustを利用する必要がありました。そこで柔軟にスケールできるようにEKS上で動かすことになりました。

EKS上でLocustを動かして、簡単な負荷試験を実施する手順などについてこの記事にまとめます。

なお、この記事ではLocustのシナリオについては深く触れません。
あくまで動作確認レベルのシナリオになります。


# 簡単に登場人物の紹介

## Locsut

Python製の負荷試験ツールです。
負荷試験のシナリオをPythonを用いて柔軟に作り込むことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/9759d767-47f6-1b71-d521-1c1d9aedb43b.png)

https://locust.io/

locustにはmaster/workerモード機能があり、これを利用することで複数インスタンス(コンテナ)を利用してスケールさせながらlocustによる負荷試験を行うことが可能です。

https://docs.locust.io/en/v0.8/running-locust-distributed.html

今回はこのmaster/workerモードを利用して、locustを動かします。

## EKS

KubernetesのAWSマネージドサービスです。
面倒なノードインスタンスをAWSが管理してくれるため、簡単にk8sを利用することができます。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/what-is-eks.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/aaecae0c-1703-5229-807b-491a36697af3.png)

## Kubernetes

最近流行りのコンテナオーケストレーション ソフトウェア/プラットフォームです。
コンテナ管理を自動的に行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/27086543-62ce-3f8c-e475-e537e67bab2f.png)

https://kubernetes.io/ja/docs/concepts/overview/what-is-kubernetes/


# 環境のセットアップ

今回作成した構成は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/b016cafc-35e6-8884-0955-4d9a9e30a977.png)

- Cloud9 : 踏み台サーバおよびIDEとして利用
- EKSクラスター：Kubernetesの基盤
- ECR : Kubernetes Deploymentのコンテナイメージを格納
- locust master/locust worker : LocustのMasterとWorkerとして動作するKubernetes Deployment
- Service : Locustへのアクセス用ポートを公開するためのKubernetes Service

VPC周りとCloud9の構築については割愛します。

## ファイル構成

cloud9上のファイル構成は以下の通りです。

```
/home/ec2-user/environment/locust/：Locust関連資材用トップディレクトリ
├── Dockerfile：DeploymentのDocker Image用のDocker file
├── locust-cluster.yaml：EKSクラスター定義ファイル
├── locust-manifest.yaml：Locust実行環境を定義したマニフェストファイル
└── locust-tasks：コンテナにコピーされるディレクトリ
    ├── locustfile.py：Locustのシナリオファイル
    └── run.sh：コンテナが実行するスクリプト
```

各ファイルの詳細については後述します。

## 準備

Cloud9にログインして、Cloud9に以下のアプリケーション/ツールをインストールします。

- [eksctl](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eksctl.html)
- [kubectl](https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/)

## EKSクラスターの作成

cloud9上でEKSクラスターの構成を定義したyamlファイルを作成します。

```plaintext:locust-cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: locust-cluster
  region: ap-northeast-1
vpc:
  subnets:
    private:
      ap-northeast-1a: { id: subnet-0xxxxxxxxxxxxxxxx }
      ap-northeast-1c: { id: subnet-0xxxxxxxxxxxxxxxx }
    public:
      ap-northeast-1a: { id: subnet-0xxxxxxxxxxxxxxxx }
      ap-northeast-1c: { id: subnet-0xxxxxxxxxxxxxxxx }
nodeGroups:
  - name: locust-nodegroup
    instanceType: t2.small
    desiredCapacity: 1
    minSize: 1
    maxSize: 30
    volumeSize: 100
```

※ 定義しているPrivate/PublicサブネットはVPCにすでに存在するものです。
(もしサブネットを指定しないと、EKSクラスターの作成と一緒にVPC/サブネットも作成されてしまいます)

以下コマンドを実行して、EKSクラスターの作成を行います。

```
eksctl create cluster -f locust-cluster.yaml
```

EKSクラスターの作成には15-20分ほど時間がかかります。

## コンテナイメージの作成

### 準備

locust-tasksというディレクトリを作成します。
そして、そのディレクトリ配下に以下ファイルを作成します。

```plaintext:run.sh
#!/bin/bash

LOCUST_COMMAND="/usr/local/bin/locust"
LOCUST_OPTS="-f /locust-tasks/locustfile.py"

if [[ "$LOCUST_MODE" = "master" ]]; then
    LOCUST_OPTS="$LOCUST_OPTS --master"
elif [[ "$LOCUST_MODE" = "worker" ]]; then
    LOCUST_OPTS="$LOCUST_OPTS --worker --master-host=$LOCUST_MASTER"
fi

echo "$LOCUST_COMMAND $LOCUST_OPTS"
$LOCUST_COMMAND $LOCUST_OPTS
```

run.shはコンテナがlocustを効率よく実行するためのシェルスクリプトです。

```plaintext:locustfile.py
from locust import TaskSet, HttpUser, task, constant
import csv
import itertools

class MyTasks(TaskSet):
    @task(1)
    def scenarioa(self):
        data = {}
        data["a"] = 1
        data["b"] = 2
        data["op"] = "add"
        self.client.post(url="/test",
                         headers={"Content-Type": "application/json"},
                         json=data)

class MyUser(HttpUser):
    wait_time = constant(1)
    tasks = [MyTasks]
```

locustfile.pyはシナリオファイルです。
詳細については後述します。

### イメージの作成

Dockerファイルを作成します。

```plaintext:Dockerfile
FROM python:3.6

# Install locustio
RUN python -m pip install --upgrade pip  && \
    python -m pip install locust

ADD locust-tasks /locust-tasks

EXPOSE 5557 5558 8089

RUN chmod 755 /locust-tasks/run.sh
ENTRYPOINT ["/locust-tasks/run.sh"]
```

その後イメージをビルドします。

```
docker build -t locust:1.0 . 
```

### ECRにイメージをPUSH

[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html)を参考にコンテナイメージをECRにPUSHします。

ECRにログインします。

```
aws ecr get-login-password | docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com 
```

※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。

イメージにタグを付与します。
```
docker tag locust:1.0 ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
```

ECRにPUSH

```
docker push ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
```

## Deployment/Serviceの作成

以下の通り、マニフェストファイルを作成します。

```plaintext:locust-manifest.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: locust
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: locust-master
  name: locust-master
  namespace: locust
spec:
  ports:
  - name: master-web
    nodePort: 30060
    port: 8089
    protocol: TCP
    targetPort: master-web
  - name: master-p1
    nodePort: 30061
    port: 5557
    protocol: TCP
    targetPort: master-p1
  - name: master-p2
    nodePort: 30062
    port: 5558
    protocol: TCP
    targetPort: master-p2
  selector:
    app: locust-master
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name:  locust-master
  namespace: locust
  labels:
    app:  locust-master
spec:
  selector:
    matchLabels:
      app: locust-master
  replicas: 1
  template:
    metadata:
      labels:
        app: locust-master
    spec:
      containers:
      - name: locust-master
        image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
        imagePullPolicy: IfNotPresent
        env:
        - name: LOCUST_MODE
          value: master
        ports:
        - containerPort: 8089
          name: master-web
          protocol: TCP
        - containerPort: 5557
          name: master-p1
          protocol: TCP
        - containerPort: 5558
          name: master-p2
          protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name:  locust-worker
  namespace: locust
  labels:
    app:  locust-worker
spec:
  selector:
    matchLabels:
      app: locust-worker
  replicas: 3
  template:
    metadata:
      labels:
        app: locust-worker
    spec:
      containers:
      - name: locust-worker
        image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
        imagePullPolicy: IfNotPresent
        env:
        - name: LOCUST_MODE
          value: worker
        - name: LOCUST_MASTER
          value: locust-master
```

上記のマニフェストについて簡単に解説します。

### 1つ目のセクション

NameSpaceを作成しています。

```
apiVersion: v1
kind: Namespace
metadata:
  name: locust
```

正直デフォルトのNameSpaceでも事足りますが、、どうせならNameSpaceも作成しようということでNameSpaceを定義しています。


### 2つ目のセクション

LoadBalancer Serviceを作成しています。

```
apiVersion: v1
kind: Service
metadata:
  labels:
    app: locust-master
  name: locust-master
  namespace: locust
spec:
  ports:
  - name: master-web
    nodePort: 30060
    port: 8089
    protocol: TCP
    targetPort: master-web
  - name: master-p1
    nodePort: 30061
    port: 5557
    protocol: TCP
    targetPort: master-p1
  - name: master-p2
    nodePort: 30062
    port: 5558
    protocol: TCP
    targetPort: master-p2
  selector:
    app: locust-master
  type: LoadBalancer
```

master/workerモードでLocustを動かすとき、masterの以下3ポートを利用します。
- 8089 : Locust Web UIへのアクセス用ポート
- 5557 : master-worker間通信用のポート(その1)
- 5558 : master-worker間通信用のポート(その2)

外部からWeb UIにアクセスは、LoadBalancerのVIPのポート8089を経由して行われます。
k8sクラスター内部におけるworkerからmasterへのアクセスはCluster IPのポート5557/5558を経由して行われます。

### 3つ目のセクション

Locust MasterのDeploymentを作成しています。

```
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name:  locust-master
  namespace: locust
  labels:
    app:  locust-master
spec:
  selector:
    matchLabels:
      app: locust-master
  replicas: 1
  template:
    metadata:
      labels:
        app: locust-master
    spec:
      containers:
      - name: locust-master
        image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
        imagePullPolicy: IfNotPresent
        env:
        - name: LOCUST_MODE
          value: master
        ports:
        - containerPort: 8089
          name: master-web
          protocol: TCP
        - containerPort: 5557
          name: master-p1
          protocol: TCP
        - containerPort: 5558
          name: master-p2
          protocol: TCP
```

※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。

masterとして起動するように環境変数を指定します。
また前述したとおり、masterにおいて3つのポートを利用するため、それらを定義します。


### 4つ目のセクション

Locust WorkerのDeploymentを作成しています。

```
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name:  locust-worker
  namespace: locust
  labels:
    app:  locust-worker
spec:
  selector:
    matchLabels:
      app: locust-worker
  replicas: 3
  template:
    metadata:
      labels:
        app: locust-worker
    spec:
      containers:
      - name: locust-worker
        image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
        imagePullPolicy: IfNotPresent
        env:
        - name: LOCUST_MODE
          value: worker
        - name: LOCUST_MASTER
          value: locust-master
```

※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。


workerとして起動するように環境変数を指定します。
またLOCUST_MASTERにはService(locust-master)を指定することで、Cluster IPのポート5557/5558を経由してmasterと接続することができます。

## DeploymentとServiceの起動

以下コマンドで起動します。

```
kubectl apply -f locust-manifest.yaml
```

DeploymentとServiceのステータスを確認します。

```
kubectl get deployment -n locust

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
locust-master   1/1     1            1           2m13s
locust-worker   3/3     3            3           2m13s
```

READYの分母と分子が全て一致していればOKです。


```
kubectl get service -n locust

NAME            TYPE           CLUSTER-IP      EXTERNAL-IP                                                                   PORT(S)                                        AGE
locust-master   LoadBalancer   10.100.217.66   axxxx3fdxxxx14f4c83xxxxab735xxxx-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com   8089:30060/TCP,5557:30061/TCP,5558:30062/TCP   16s
```

EXTERNAL-IPが表示されていればOKです。


## Locustへ接続

ServiceのEXTERNAL-IPのポート8089にWebブラウザよりアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/4963533d-638f-0519-bb85-03f7e1a92312.png)


# 実際に動かす

実際にテスト用のAPI Gatewayを作成して、それに対して負荷試験を実施します。

## テスト用API Gatewayの作成

以下の簡単な四則計算を行うLambda関数をAPI GatewayにPostメソッドとして登録します。

```
console.log('Loading the calculator function');

exports.handler = function(event, context, callback) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    if (event.a === undefined || event.b === undefined || event.op === undefined) {
        callback("400 Invalid Input");
    }
    
    var res = {};
    res.a = Number(event.a);
    res.b = Number(event.b);
    res.op = event.op;
    
    if (isNaN(event.a) || isNaN(event.b)) {
        callback("400 Invalid Operand");
    }

    switch(event.op)
    {
        case "+":
        case "add":
            res.c = res.a + res.b;
            break;
        case "-":
        case "sub":
            res.c = res.a - res.b;
            break;
        case "*":
        case "mul":
            res.c = res.a * res.b;
            break;
        case "/":
        case "div":
            res.c = res.b===0 ? NaN : Number(event.a) / Number(event.b);
            break;
        default:
            callback("400 Invalid Operator");
            break;
    }
    callback(null, res);
};
```

curlコマンドで正常に動作することを確認します。

```
curl -X POST -H "Content-Type: application/json" -d '{"a":1, "b":2, "op":"add"}' https://<作成したAPI Gatewayのエンドポイント>

{"a":1,"b":2,"op":"add","c":3}
```

## 負荷試験開始

前述したシナリオファイルは以下の通りです。

```locustfile.py
from locust import TaskSet, HttpUser, task, constant
import csv
import itertools

class MyTasks(TaskSet):
    @task(1)
    def scenarioa(self):
        data = {}
        data["a"] = 1
        data["b"] = 2
        data["op"] = "add"
        self.client.post(url="/test",
                         headers={"Content-Type": "application/json"},
                         json=data)

class MyUser(HttpUser):
    wait_time = constant(1)
    tasks = [MyTasks]
```

上記のシナリオファイルを読み込ませることで**(LocustのWeb UIのHostに指定したURL)/test**に対してリクエストを送信することができます。

LocustのWeb UIにAPI Gatewayのエンドポイント名を入力して、Starting Swarmingをクリックします。

すると負荷試験が始まります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/7106e676-0c29-5cce-e588-cd1700227c32.png)


master/workerモードではWorkersタブから、各Podsの稼働状況を確認することが可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435280/5f7fc519-988a-41ad-e962-c144588ee967.png)