みんなの銀行 クラウドネイティブな分散負荷試験環境の選定と構築

[ゼロバンク・デザインファクトリー](https://www.zdf.jp/) アルバイトの菊池兼矢です。
今回は、GKE上にk6-operatorを用いて負荷試験環境を構築したため、その知見を共有します。

2024年3月時点の内容です。
## 自己紹介
千葉工業大学 情報ネットワーク学科に所属しております。
個人でKubernetesを用いたプライベートクラウドの開発・運用を行っており、Kubernetes関連のエコシステムやコンテナ技術等に興味があります。本アルバイトを通じてSRE業務を体験したいと思い参加しました。

# 概要

[みんなの銀行](https://www.minna-no-ginko.com/)の[BaaS](https://baas.minna-no-ginko.com/)開発で使用する、クラウドネイティブな分散負荷試験環境を選定して、GKE上に構築しました。

# 背景

みんなの銀行では、アクセス集中時でも安定したサービス提供を行えるようにするため、システム開発時に負荷試験を行っています。その際、現状では開発者が使用するPCからLocustを使用して負荷試験を行っています。

開発者が使用するPCから負荷試験を行っていたため、以下のような課題がありました。
- 分散負荷試験を行う際、複数台のPCをセットアップする必要がある
- 複数のライブラリを使用してLocust環境を構築しているため、環境構築が面倒

そこで、GKE(Google Kubernetes Engine)上のクラスタから容易に分散負荷試験を行える環境を構築することにしました。

:::note info
分散負荷試験とは？

テスト対象のサービスに対して、複数のコンピュータから同時に負荷をかけること。同時に多くの人が利用した際に、サービスにかかる負荷を再現できる。
:::

# 負荷試験環境の調査・k6の選定

### Locustの問題

Locustを使用した負荷試験環境をGKE上に構築する場合、以下のことが問題になりました。
1. テストシナリオを更新する場合、マスターノードとワーカーノード上のテストシナリオファイルを更新する必要があり、大変である
2. テスト開始や結果の確認を簡単に行うためには、LocustのWebGUIへのアクセスが必要であり、セキュリティの観点から別途認証機能を追加する必要がある

:::note info
用語
マスターノード: 複数のワーカーノードを制御するLocustノード
ワーカーノード: 負荷をかけるLocustノード
:::

そこで、現在使用している負荷試験ツールであるLocustでなければいけない理由もなかったため、より優れたツールがないか、調査・検討を行いました。

### k6の優れている点

調査・検討の結果、k6が優れていたため、導入することにしました。

#### Locustとの比較
1. k6の開発元がメンテナンスしているk6-operator(k6のKubernetes Operator版実装)を利用すると、k6-operatorがテストシナリオを集中制御してくれる
2. 負荷試験の実行部分にArgo Workflows、結果確認部分にNew Relicを使用することで、WebGUIへのアクセスが不要となり、認証機能が不要となる（どちらのツールも導入済）

#### k6の良いところ
- k6-operatorはk6の開発元がメンテナンスしているため、k6-operatorのアップデート時に私たちが行うメンテナンスのコストが低いと考えられる
- TestRun(k6-operatorのCRD)をArgo Workflows上から操作することで、容易にテストの実行ができる
- 負荷試験結果をNew Relicに送信することで、より柔軟なUIで負荷試験結果を閲覧できる
- JavaScriptでテストシナリオが書けるため、馴染みのある言語で記述できる（これはPythonで書けるLocustも同様）

#### k6の実装方法の選定

k6で分散負荷試験を行うには、以下3通りの実装方法があります。
1. Grafana Cloud
    Grafana Cloud上から負荷試験を行う方法
1. Grafana Cloud/Private Load Zoneの使用
    テストの管理やテスト結果の保存・閲覧にGrafana Cloudを使用するが、負荷をかけるノードを自分で用意する方法
1. k6-operator
    Grafana Cloud等を使用せずに、Kubernetesで完結させて負荷試験を行う方法

私たちの負荷試験対象サーバーはGoogle Cloudのプライベートネットワーク上に存在するため、1.は使用できません。そのため残された2.と3.の選択肢で検討を行いました。その結果、まずは3.の方法でスモールスタートすることにしました。

# 負荷試験環境の設計

![設計図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3717516/27514206-9450-a3e1-9619-6cf75f600cda.png)

### k6-operatorのデプロイ
GKE上にk6-operatorをデプロイします。
その際、アップデート・構成変更・リソース制限の容易性を考慮して、k6-operator system本体と負荷試験を行うnamespaceを別のものにしました。
| namespace名 | 役割 |
| ---- | ---- |
| k6-operator-system | k6-operator本体を実行 |
| k6-runner | 負荷試験を実行 |

### 負荷試験結果をNew Relicに送信
テスト実行時はTestRunからPrometheus Remote Writeを使用して、試験結果をリアルタイムにNew Relicへ送信します。
試験結果をリアルタイムに送信しているため、試験開始直後からNew RelicのDashboardで結果を確認できます。

#### Prometheus Remote Writeの選定理由

k6は拡張機能を含めると[多くの](https://k6.io/docs/results-output/real-time/)メトリクス送信形式に対応しています。
New Relicに送信可能なメトリクス形式は以下の2つが見つかりました。
1. [Prometheus Remote Write](https://k6.io/docs/results-output/real-time/prometheus-remote-write/)
1. [StatsD](https://k6.io/docs/results-output/real-time/new-relic/)

そのうち2.はコア機能でサポートされなくなるため、1.を使用することにしました。
[サポートされなくなる理由](https://github.com/grafana/k6/issues/2982#issuecomment-1765304269)

### Argo Workflowsの実行
テスト実行時はGitlabからテストシナリオをPersistentVolumeにpullして、TestRunを実行します。

### この設計のメリット
- テストシナリオのpull等も含めてGKE上に構築しているため、テスト実行時にローカルの環境構築を行うことが不要であり、容易に分散負荷試験を行える
- GKE上で分散負荷試験を行うため、k6側のリソース上限が問題になりにくい
- 認証は既存のArgo Workflows(テスト実行時)とNew Relic(結果閲覧時)で行うため、分散負荷試験環境に別途認証機能を付けることが不要である

# 負荷試験環境の実装

### k6-operatorのデプロイ

k6-operatorはアップデートや構成変更を容易とするため、helmfile templateでマニフェストを事前生成しました。マニフェスト内容に問題がないことを確認後にGKE上のクラスタにマニフェストを適用しました。

### 負荷試験実行用マニフェストを作成

Argo Workflowsを用いて、以下のマニフェストをデプロイします。
※最終的にはArgo Workflowsから実行することを目標としていますが、現段階ではArgo Workflowsが構築中のため、kubectlから直接実行しています。

```
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
  name: k6-test
  namespace: k6-runner
spec:
  parallelism: 2
  script:
    configMap:
      name: k6-test-scenario
      file: archive.tar
  separate: true
  runner:
    image: [カスタムイメージのURL]
    env:
      - name: K6_PROMETHEUS_RW_SERVER_URL
        valueFrom:
          secretKeyRef:
            name: newrelic-prometheus-rw
            key: uri
      - name: K6_OUT
        value: experimental-prometheus-rw
      - name: K6_PROMETHEUS_RW_TREND_STATS
        value: p(95)
      - name: K6_PROMETHEUS_RW_PUSH_INTERVAL
        value: 1s
```

#### .specで指定しているパラメーターについて

| パラメーター名 | パラメーターの内容                            | 設定値 |
| ----          | ----                                         | ----  |
| parallelism   | 分散負荷試験の並列数(Pods数)                  | 2      |
| separate      | trueのとき、異なるノード上にPodsをスケジュール | true   |
| script        | Argo Workflowsからの実行ではPersistentVolume使用するが、一時的にkubectlで実行しているため、configmapを参照する | アップロードするテストシナリオを指定 |

#### .spec.runner.envで指定している環境変数について

##### K6_PROMETHEUS_RW_SERVER_URL

New RelicのPrometheus Remote WriteのURLをSecretにて指定します。

###### 手順
1. New RelicのDashboardにて、ライセンスキーとPrometheusサーバー名を生成する
1. 以下のコマンドでURLとライセンスキーとPrometheusサーバー名をBase64エンコードする
    ```
    echo -n 'https://metric-api.newrelic.com/prometheus/v1/write?X-License-Key=[1で生成したライセンスキー]&prometheus_server=[1で生成したPrometheusサーバー名]' | base64 -w 0
    ```
    オプション(**重要**)の説明
    ※ -n   : 改行を付加せず出力する
    ※ -w 0 : 出力を改行せずに出力する
1. Secretを作成する
    ```
    apiVersion: v1
    kind: Secret
    metadata:
      name: newrelic-prometheus-rw
    type: Opaque
    data:
      uri: [２で生成した文字列]
    ```
1. 3で作成したSecretをk6-runner namespaceに適用する

##### K6_OUT

試験結果の出力方法を指定します。

`experimental-prometheus-rw`を指定することで、`K6_PROMETHEUS_RW_SERVER_URL`で指定した方法でメトリクスを送信します。

##### K6_PROMETHEUS_RW_TREND_STATS

追加で送信するメトリクスの種類を指定します。

`p(95)`を指定することで、試験結果の95パーセンタイルの値を出力します。

##### K6_PROMETHEUS_RW_PUSH_INTERVAL

メトリクスを送信する間隔です。

#### .spec.runner.imageで指定するカスタムイメージについて

##### k6で拡張機能を使用する

[みんなの銀行は(FAPI)Financial-grade APIに対応](https://corporate.minna-no-ginko.com/information/corporate/2022/09/29/227/)しています。
そのため、負荷試験環境もFAPIに対応する必要があります。

FAPIでは楕円曲線暗号が使用されています。しかし、k6は標準で楕円曲線暗号に対応していません。理由は、k6はJavaScriptランタイムにGojaを使用しており、Gojaは標準で楕円曲線暗号に対応していないためです。

そこで、k6の[拡張機能](https://k6.io/docs/extensions/get-started/create/javascript-extensions/)を使用して、Go言語に存在する楕円曲線暗号のライブラリをJavaScriptからラッピングして使用することにしました。

##### k6-operatorで拡張機能を使用する

この機能をk6-operatorで使用するためには、カスタムイメージの作成が必要です。
そのため、以下のDockerfileを用いてカスタムイメージを作成しました。
```
# Build the k6 binary with the extension
FROM golang:1.20 as builder

RUN go install go.k6.io/xk6/cmd/xk6@v0.11.0
# Feel free to add other extensions using the '--with ...'.
RUN xk6 build \
    --with github.com/szkiba/xk6-jose@v0.1.3 \
    --output /k6

# Use the operator's base image and override the k6 binary
FROM grafana/k6:0.49.0
COPY --from=builder /k6 /usr/bin/k6
```
上記の`--with`で追加するライブラリを複数指定できます。

# 負荷試験の実行

最終的にはArgo Workflows WebGUIから実行しますが、Argo Workflowsが現在構築中のため、現状ではkubectlで実行しています。

### GKE上のクラスタで分散負荷試験を実行

```
k6 archive main.js &&\
kubectl create configmap k6-test-scenario --from-file=archive.tar -n k6-runner &&\
kubectl apply -f ./no-use-workflows/test-run.yaml -n k6-runner &&\
rm archive.tar
```

### テストの進行状況の閲覧

New RelicのDashboardを開く

以下のDashboardで試験結果を確認できます。
![new_relic_dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3717516/2d925ccd-ea0d-7817-6702-fe5ccced5acf.png)

### テスト完了を待機

```
watch kubectl get TestRun -n k6-runner
```

### テストに使用したリソースを削除

```
kubectl delete TestRun k6-test -n k6-runner ;\
kubectl delete cm k6-test-scenario -n k6-runner
```

## まとめ

テストシナリオとkubectlのみで負荷試験が行えるようになったため、事前の環境構築なしで分散負荷試験が行えるようになりました。今後は、ArgoWorkflowsのテンプレートやNewRelicのダッシュボードを開発して、使いやすいテストツールを完成させたいと思います。

## 感想

インフラやプライベートクラウドに興味があり、個人で勉強したり構築したりしていました。今回のアルバイトでは、それらが実際のサービスでどのように活用されているのかを少しだけですが知ることができて、勉強になりました。
また、アルバイトを通して、自分の知らないツールや物事の進め方を教えていただいたり、提案した内容や成果物に対してレビューしていただき、それを元にしながら開発していくことでより良いものが作れて、楽しかったです。
特に選定に関しては、今まで構築する前に時間をかけてしっかりと調査したことがありませんでしたが、それをやったため今回は選定後に足りない機能がないなどで、余計な機能拡張をしたり、手戻りすることがなく、効率的にできたのは良かったです。

## 参考

[k6 ドキュメント](https://k6.io/docs/)
[k6 xk6拡張機能](https://k6.io/docs/extensions/get-started/create/javascript-extensions/)
[k6-operator ドキュメント](https://github.com/grafana/k6-operator)
[New Relic 設定方法](https://newrelic.com/jp/instant-observability/k6-prometheus)
[Argo Workflows ドキュメント](https://argo-workflows.readthedocs.io/en/latest/)
[Argo Workflows 設定例](https://github.com/argoproj/argo-workflows/blob/main/examples/README.md)