LocustをKubernetes内でUIおよびHeadlessモードで使う


[Locust](https://docs.locust.io/en/stable/index.html)はPythonベースの負荷試験ツールで、JMeterやTsungの不満を解消するために作られており、有名どころのサービスの負荷試験にも使われているらしい。
これ、使ってみると非常に便利だが、Kubernetes向けの導入方法が'23/8時点では[見当たらなかった](https://docs.locust.io/en/stable/installation.html)。
負荷対象が`kind: Ingress`等で外部にサービスを公開していればいいが、公開されておらず`kind: Service`+`type: ClusterIP`経由でアクセスしたい場合とかはクラスタ内部から負荷を掛ける必要がある。
今回はそのための確認を行った時のメモ。
Locustを使うシーンとしては、UIから手動で実行するケースと、UIなし(Headless）で自動で実行するケースが考えられるため、それぞれKubernetesで動かしてみる。

なお、ここでの確認手順はk8sのディストリビューションに依存しないため、どのk8s環境でも利用できる。はず。

## WebUIのLocustコンテナを動かす
元々Locustのイメージが提供されているので、それを使う。
最初に、`locustfile.py`をConfigMapで定義する。
テストケースに応じて差し替えるとよい、
```yaml
cat << EOF > ./locustfile.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: locustfile
data:
  locustfile.py: |
    from locust import HttpUser, task
    
    class LocustTest(HttpUser):
        @task
        def test_case1(self):
            self.client.get("/")
EOF
```
Locust本体を`Deployment`と`Service`で定義する。`locustfile.py`は`ConfigMap`をmountして-fで渡す。
```yaml
cat << EOF > ./locust.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: locust
spec:
  replicas: 1
  selector:
    matchLabels:
      app: locust
  template:
    metadata:
      labels:
        app: locust
    spec:
      containers:
      - name: locust
        image: locustio/locust
        args:
        - -f
        - /mnt/locust/locustfile.py
        ports:
        - containerPort: 8089
        volumeMounts:
        - name: locustfile-volume
          mountPath: /mnt/locust
      volumes:
      - name: locustfile-volume
        configMap:
          name: locustfile
---
apiVersion: v1
kind: Service
metadata:
  name: locust
spec:
  selector:
    app: locust
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8089
  type: LoadBalancer
EOF
```
それぞれデプロイし、LocustのExternalIPを確認する。
```
kubectl apply -f ./locust.yaml -f ./locustfile.yaml
kubectl get svc locust -o jsonpath={.status.loadBalancer.ingress[0].ip}
```
ExternalIPにアクセスすると、無事にUIが表示される。ここでは試しにdefaultのNamespaceに立てたnginxに対して負荷を掛けてみる。
![1691880405785.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/1e3089f4-2012-191a-760b-f7e336da5e9d.png)
実行すると、Failsにカウントが増えることなくリクエストが処理されており、問題ないことが確認できた。
![1691880447669.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/2eae223c-50d6-1da6-2626-312af01ceb2f.png)

## Headlessモードで動かす
Locustは`--headless`を付けて実行すると、Headlessモードとして動作し、指定の負荷を実行した後に終了する。
コマンドとしては以下のような感じになる。
```sh
locust --headless -H $TARGET_URL -u $NUM_USERS -r $SPAWN_RATE -t $RUN_TIME
```
オプションはそれぞれ以下の意味となる。
- `-H`: Host
- `-u`: 最大ユーザ数
- `-r`: 1秒あたりの増加数
- `-t`: 実行時間。これを過ぎるとコマンドは終了する。指定しないと無制限。

このオプションや環境変数を最初コマンドに渡そうと思ったが、この手の設定を設定ファイルとして用意して渡すことも出来る。
```sh
locust --config=master.conf
```
`master.conf`には[公式の記述](https://docs.locust.io/en/stable/configuration.html)によると、以下のような内容を記述でき、環境変数で設定したものがまとめて設定できるようになっている。。
```conf
locustfile = locust_files/my_locust_file.py
headless = true
master = true
expect-workers = 5
host = http://target-system
users = 100
spawn-rate = 10
run-time = 10m
```


ということで、上記のmaster.confを`ConfigMap`で作成し、実行側は`Job`で作成してmountして利用する。
`locustfile.py`とは別に以下の`ConfigMap`を作成する。
```yaml
cat << EOF > ./locust-master-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: locust-master-config
data:
  master.conf: |
    locustfile = /mnt/locust/locustfile.py
    headless = true
    host = http://nginx.default.svc
    users = 100
    spawn-rate = 10
    run-time = 10m
EOF
```
`Job`は以下のようになる。
```yaml
cat << 'EOF' > ./locust-headless.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: locust-headless
  labels:
    app: locust-headless
spec:
  backoffLimit: 1
  template:
    metadata:
      labels:
        app: locust-headless
    spec:
      restartPolicy: Never
      containers:
      - name: locust-headless
        image: locustio/locust
        args:
        - --config
        - /mnt/locust-master-config/master.conf
        volumeMounts:
        - name: locustfile-volume
          mountPath: /mnt/locust
        - name: locust-master-config
          mountPath: /mnt/locust-master-config
      volumes:
      - name: locustfile-volume
        configMap:
          name: locustfile
      - name: locust-master-config
        configMap:
          name: locust-master-config
EOF
```

起動する。`ConfigMap`の`locustfile`はUI確認時にデプロイ済みなのでここでは割愛しているが、必要なのでHeadless版から試している人はUI版で実施している内容を実施しておくこと。
```sh
kubectl apply -f ./locust-master-config.yaml -f ./locust-headless.yaml
```

ログを見ると、しっかり負荷が掛かっているようだ。
```sh
kubectl logs locust-headless-4sx25
[2023-08-12 23:51:26,093] locust-headless-4sx25/INFO/locust.main: Run time limit set to 600 seconds
[2023-08-12 23:51:26,094] locust-headless-4sx25/INFO/locust.main: Starting Locust 2.16.1
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                         0     0(0.00%) |      0       0       0      0 |    0.00        0.00

[2023-08-12 23:51:26,128] locust-headless-4sx25/INFO/locust.runners: Ramping to 100 users at a rate of 10.00 per second
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /                                                                               1057     0(0.00%) |     13       5      71     10 |    0.00        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      1057     0(0.00%) |     13       5      71     10 |    0.00        0.00
```