k6をGCP環境(Cloud Build)で実行する際のTips

# はじめに
[k6](https://k6.io/)という負荷テストツールをGoogle Cloud （以降、GCP）環境で実施する機会がありました。
k6はオープンソースの負荷テストツールで、 JavaScript でテストシナリオを作成して実施することができます。
k6についての詳細は Appendix を参照ください。

※ GCPの各種サービスについて詳細な説明は省略しています。
※ k6 のスクリプトの内容については今回は触れず、GCP上で実行することにフォーカスして記載しています。

今回は GCPの Cloud Build で k6 を実行する機会がありました。
Cloud Build 上で実行するメリットや、留意点を備忘としてまとめます。

# サービスの選定
今回は 小規模な、簡易的にコマンドワンショットで k6スクリプト を実行するイメージでした。
これに適切なサービスはどれか、ということで以下を検討しました。
大規模かつ複雑な処理を必要とする場合は GKE などを検討するのがよさそうです。

- Google Compute Engine (GCE)
- Cloud Build
- Cloud Run Jobs

GCEはインスタンスの管理なども加わることを考えると、なるべく避けたい選択肢でした。
従量制課金のメリットを考えると Cloud Build 、もしくは Cloud Run Jobs が望ましいと考えました。

利用可能なスペックで２つを比較すると、 Cloud Run は1コンテナあたり [8CPU](https://cloud.google.com/run/docs/configuring/services/cpu?hl=ja)/[32Gi](https://cloud.google.com/run/docs/configuring/services/memory-limits?hl=ja)まで設定可能です。

対してCloud Build はデフォルトで 32CPU/32GBi、プライベートプールを使用することで 32CPU/128Gi まで設定可能です。([Cloud Build の料金](https://cloud.google.com/build/pricing?hl=ja))

試験実施後は k6 の実行結果を集計する必要がありましたが、 Cloud Run も Cloud Build もCloud Loggingに出力されるため、集計に関しては特に困らないだろうと考えていました。

また、 Cloud Build での実行は k6 のブログでも紹介されています。([How to Perform Load Testing with k6 using Google Cloud Build](https://k6.io/blog/integrating-k6-with-google-cloud-build/))

以上から、設定可能なスペックからも今回は Cloud Build を選択しました。

# Cloud Build
Cloud Build での基本的な実行については、公式ブログの [How to Perform Load Testing with k6 using Google Cloud Build - setting-up-the-cloud-build-workflow)](https://k6.io/blog/integrating-k6-with-google-cloud-build/#setting-up-the-cloud-build-workflow) がわかりやすいです。
上記ブログを参考に、 docker-compose で実行する手順を以下に記載します。

### docker-compose のビルド
docker-compose は、[Cloud Builders Community](https://cloud.google.com/build/docs/cloud-builders?hl=ja)で提供されているため、これを利用します。

```
# Clone the entire cloud builders community repo onto your hard drive
git clone https://github.com/GoogleCloudPlatform/cloud-builders-community

# Navigate into the docker-compose folder
cd cloud-builders-community/docker-compose/

# Install docker-compose builder into your account
gcloud builds submit --config=cloudbuild.yaml .
```

### Cloud Buildでの実行
```docker-compose.yml
version: "3.4"
services:
  k6:
    image: grafana/k6:latest
    command: run /scripts/script.js
    volumes:
      - ./scripts:/scripts
```

```cloudbuild.yaml
steps:
# Load Test
- name: "gcr.io/$PROJECT_ID/docker-compose"
  args: ["up"]
# options:
#   machineType: 'E2_HIGHCPU_32'  # 32 vCPUs 32GB MEM
```
コメントアウトしていますが options を有効にすることで、特定のCPU/メモリを指定することが可能です。（上限は上記に記載のとおりです。）

以上のファイルを用意し、 `gcloud build` を実行することで用意したスクリプトをマウントして k6 を実行することができます。
マウントしているパス、ファイル名は適宜修正してください。

```
$ gcloud builds submit --region=asia-northeast1 --config=cloudbuild.yaml .
Creating workspace_k6_1 ... done
Attaching to workspace_k6_1
k6_1  |
k6_1  |           /\      |‾‾| /‾‾/   /‾‾/
k6_1  |      /\  /  \     |  |/  /   /  /
k6_1  |     /  \/    \    |     (   /   ‾‾\
k6_1  |    /          \   |  |\  \ |  (‾)  |
k6_1  |   / __________ \  |__| \__\ \_____/ .io
k6_1  |
k6_1  |      execution: local
k6_1  |         script: /scripts/script.js
k6_1  |         output: -
k6_1  |
k6_1  |      scenarios: (100.00%) 1 scenario, 15 max VUs, 40s max duration (incl. graceful stop):
k6_1  |               * testscenario: 15 looping VUs for 10s (gracefulStop: 30s

<< 省略 >>

k6_1  | running (10.3s), 00/15 VUs, 150 complete and 0 interrupted iterations
k6_1  | testscenario ✓ [ 100% ] 15 VUs  10s
workspace_k6_1 exited with code 0
```

以上で、単純にk6スクリプトを Cloud Build 上で実行する手順について記載しました。

## モニタリング
次に、Cloud Build を使用する上でデメリットについて記載します。
それはCloud Build 実行環境のCPU使用率やメモリ使用率をGCP上で取得する方法が難しいことです。
例えば k6 で思うようにパフォーマンスが出ない際、 Cloud Build 側なのか、サーバー側なのか、どちらがボトルネックになっているか判断が難しいです。

調べたところ、サイドカーとして New Relicにメトリクスを送信する方法がかなり便利そうでした。
([Newrelic, k6, Cloud Buildでらくらく負荷試験](https://tech.emotion-tech.co.jp/entry/2022/12/03/100000))
当方では New Relic 環境がなかったので、諦めました・・・

他にも、[Grafana Cloud k6](https://k6.io/blog/integrating-k6-with-google-cloud-build/#running-cloud-tests-2)を利用するという手もありましたが、今回はGCP上で完結させたかったため、見送りました。

サポートともやり取りしたところ、ワークアラウンドとしてfreeコマンドをバックグラウンドでループして実行する方法を提示されたので、そちらを実施しました。
サンプルは以下になります。

```cloudbuild.yaml
steps:
#This step runs free in the backround in a loop
- id: service1
  name: gcr.io/cloud-builders/docker
  waitFor: ['-']
  args:
  - run
  - '-d'
  - '--name=service1'
  - ubuntu
  - bash
  - '-c'
  - |
    while true ; do free ; sleep 2 ; done

# This step batch-retrieves logs present at the time of execution
- name: gcr.io/cloud-builders/docker
  waitFor: ['service1']
  args: [logs, '-f', service1]

# Configure the step that you would like to monitor for memory usage here
- id: actual_step
  name: 'gcr.io/$PROJECT_ID/docker-compose'
  args: [
    'run',
  ]
options:
  machineType: 'E2_HIGHCPU_32'  # 32 vCPUs 32GB MEM
    
# This step removes container service1
- waitFor: ['actual_step']
  name: gcr.io/cloud-builders/docker
  args: [rm, -f, service1]
```

これを実行すると以下のような出力となります。
```
<< 省略 >>
Step #2 - "actual_step": Creating workspace_k6_run ... done
Step #2 - "actual_step":
Step #2 - "actual_step":           /\      |‾‾| /‾‾/   /‾‾/
Step #2 - "actual_step":      /\  /  \     |  |/  /   /  /
Step #2 - "actual_step":     /  \/    \    |     (   /   ‾‾\
Step #2 - "actual_step":    /          \   |  |\  \ |  (‾)  |
Step #2 - "actual_step":   / __________ \  |__| \__\ \_____/ .io
Step #2 - "actual_step":
Step #2 - "actual_step":      execution: local
Step #2 - "actual_step":         script: /scripts/script.js
Step #2 - "actual_step":         output: -
Step #2 - "actual_step":
Step #2 - "actual_step":      scenarios: (100.00%) 1 scenario, 25000 max VUs, 1m30s max duration (incl. graceful stop):
Step #2 - "actual_step":               * testscenario: 25000 looping VUs for 1m0s (gracefulStop: 30s)
Step #2 - "actual_step":
Step #1:                total        used        free      shared  buff/cache   available
Step #1: Mem:         8147696      909356     6254032         792     1254432     7238340
Step #1: Swap:              0           0           0
Step #2 - "actual_step":
Step #2 - "actual_step": Init           [  11% ] 02665/25000 VUs initialized

<< 省略 >>

Step #2 - "actual_step":
Step #2 - "actual_step": running (0m53.8s), 25000/25000 VUs, 141015 complete and 0 interrupted iterations
Step #2 - "actual_step": testscenario   [  90% ] 25000 VUs  0m53.8s/1m0s
Step #2 - "actual_step":
Step #2 - "actual_step": running (0m54.7s), 25000/25000 VUs, 146046 complete and 0 interrupted iterations
Step #2 - "actual_step": testscenario   [  91% ] 25000 VUs  0m54.7s/1m0s
Step #1:                total        used        free      shared  buff/cache   available
Step #1: Mem:         8147696     8124532      120460         792       80124       23164
Step #1: Swap:              0           0           0

<< 省略 >>
```

2秒おきに `free` コマンドが実行されています。
起動したタイミングではメモリに余裕がありますが、負荷をかけていくにつれメモリ使用量が圧迫していることが見て取れます。
以上から、ワークアラウンドではありますが、必要なインスタンスサイズの特定など、最低限のボトルネックの特定には使えそうでした。

本当は [Opsエージェント](https://cloud.google.com/monitoring/agent/ops-agent?hl=ja) を利用したかったのですが、Compute Engineのみをサポートしているため諦めました。


# おわりに
簡易的な実行だったのでこの方法を採用しましたが、今振り返れば実況環境を GCE や Cloud Run にすることで、GCPネイティブな方法でパフォーマンスが評価できたかもしれません。
New Relicなどに送信する環境がない場合は ちゃんと Grafana Cloud k6 を使うか、 Cloud Build に固執せず、に実行環境を変更するというのも検討すべきかもしれません。

# Appendix
- [負荷テストツール「k6」入門](https://zenn.dev/pharmax/articles/98ed49994cdaf2)
- [負荷テストを手軽にできるツール「k6」を試してみた](https://zenn.dev/rescuenow/articles/8349deb470470e)