はじめに
CI/CD で負荷試験を実行するために Grafana k6 を使ってみたので導入部分を記事にしてみました
Grafana k6 とは
オープンソースで開発されている負荷試験用ツール
並列実行数や実行時間を指定して API を呼び出し、閾値を設けて試験の合否を判定できます
2025年5月6日にバージョン 1.0.0 が公開されました1
k6 を使ってよかった点
使っていて感じたメリットを書きました
- シングルバイナリ
バイナリファイルをダウンロードして配置するのみで実行できるため、リモート環境でも手軽に試せます - TypeScript でテストコードを作成可能
npm でtypes/k6
が提供されているため、専用コマンドにも TypeScript の補完が使えます
また、引数に直接.ts
拡張子のファイルを指定して実行できます - バイナリが重すぎない
zip 化して Lambda Layer として登録できます
負荷試験を Lambda で実行する
手軽にテストするため Lambda で k6 を呼び出す関数を実装しました
- リモート環境で編集しながら実行できます
- API やパイプラインに組み込みやすいです
- Step Functions を使って並列実行やリトライ処理を簡単に設定できます
テストする API
REST API と GraphQL API を検証するために API Gateway と AppSync を使って API を作成します
今回はテスト用なので API Gateway は mock で作成して AppSync もデータソースのない JavaScript リゾルバで実装しました
他のサービスに接続せず、単純な文字列のみを返す API にしています
また、条件を揃えるためにどちらの API も API Key を設定しました
k6 バイナリを Lambda Layer に登録
-
Git Hubから
linux-arm64.tar.gz
をダウンロードして解凍します
linux-amd64.tar.gz
でも大丈夫です、その場合は以降で指定するアーキテクチャーをarm64
からx86_64
に読み替えてください -
bin
という名前のディレクトリを作成して、解凍したk6
のバイナリファイルを入れて zip 化します. └── bin/ └── k6 ← k6 バイナリ
- AWS コンソールで Lambda Layer を作成します
zip ファイルは 30 MB より少し大きいくらいのサイズなので、50 MB 以内に収まります - アーキテクチャーに
arm64
を指定して、ランタイムにNode.js
を指定して作成します
K6 を実行する Lambda
REST API 用の Lambda と GraphQL API 用の Lambda を別々に作成します
- ランタイム
Node.js(今回はNode.js 22.x
を指定) - アーキテクチャ
arm64(Lambda Layer とそろえます) - タイムアウト
デフォルトが 3 秒なのでタイムアウトしないように 30 秒に変更します - メモリ
テストで使用されるメモリがデフォルトの 128MB を超えるので 1024MB 以上に設定します2
-
環境変数
API_KEY
を定義します -
ディレクトリ構成
. ├── handler.mjs ← Lambda ハンドラー ├── scripts/ │ ├── k6.ts ← k6 テストスクリプト(TypeScript) └── layers/ └── k6-layer.zip ← /bin/k6 を含んだ layer(Lambda では /opt/bin/k6 に展開)
-
hander.mjs
import { spawnSync } from "child_process"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; export async function handler() { const scriptPath = join( dirname(fileURLToPath(import.meta.url)), "scripts", "k6.ts" ); const options = [ "run", "--env", `API_KEY=${process.env.API_KEY}`, scriptPath, ]; const result = spawnSync("/opt/bin/k6", options, { encoding: "utf-8" }); console.info("k6 stdout:", result.stdout); if (result.error || result.status !== 0) { throw new Error(); } }
Lambda Layer で登録された k6 をコマンドにして、引数に
k6.ts
ファイルを指定して実行させます
エラーを受け取った場合はエラー終了にして、閾値を下回った場合も Lambda を異常終了させます
API Key は環境変数で指定いしてprocess.env.API_KEY
をコマンドのオプションとして渡しています -
k6.ts
下記は GraphQL API 用の実装例ですimport http from "k6/http"; import { check, sleep } from "k6"; export const options = { vus: 1000, duration: "10s", thresholds: { http_req_failed: ["rate<0.01"], http_req_duration: ["p(95)<1000"], checks: ["rate>0.9"], }, }; export default function () { const params = { headers: { "Content-Type": "application/json", "x-api-key": __ENV.API_KEY, }, }; const query = JSON.stringify({ query: "query MyQuery { test }" }); const res = http.post( "https://<エンドポイント>.appsync-api.<リージョン>.amazonaws.com/graphql", query, params ); const body = JSON.parse(res.body); check(body, { "body check": (b) => b.data.test.includes("Hello from AppSync!"), }); sleep(1); }
パラメータを以下のように設定します
- vus: 並列実行数(今回は 1000 件)
- duration: 試験実行時間(今回は 10 秒)
- thresholds
- http_req_failed: リクエスト失敗率(上記では 1% 未満)
- http_req_duration: レスポンス応答時間(上記では 95% が 1 秒未満)
- checks: 関数の中で設定した check の成功割合(上記では 90% より多い)
コマンドの引数で渡した API Key は
__ENV
で取得して設定します
また、API 実行ごとに 1 秒間の待機時間を入れています
Step Function で並列実行
並列にテストを走らせる Step Functions を作成します
今回は、REST API と GraphQL API それぞれを試験する Lambda を並列で実行する処理を記載しました
{
"StartAt": "Parallel",
"States": {
"Parallel": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "API Gateway Load Test",
"States": {
"API Gateway Load Test": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": {
"FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:<関数名>:$LATEST"
},
"End": true
}
}
},
{
"StartAt": "AppSync Load Test",
"States": {
"AppSync Load Test": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": {
"FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:<関数名>:$LATEST"
},
"End": true
}
}
}
],
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Comment": "error",
"Next": "Fail"
}
],
"Next": "Succeed"
},
"Succeed": {
"Type": "Succeed"
},
"Fail": {
"Type": "Fail"
}
},
"QueryLanguage": "JSONata"
}
実行すると以下のような画面が表示されます
Lambda の実行結果は CloudWatch で確認できます
実行結果
REST API と GraphQL API それぞれで以下のような実行結果が得られます
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: /var/task/scripts/k6.ts
output: -
scenarios: (100.00%) 1 scenario, 1000 max VUs, 40s max duration (incl. graceful stop):
* default: 1000 looping VUs for 10s (gracefulStop: 30s)
running (00.9s), 1000/1000 VUs, 0 complete and 0 interrupted iterations
default [ 9% ] 1000 VUs 00.9s/10s
running (01.9s), 1000/1000 VUs, 984 complete and 0 interrupted iterations
default [ 19% ] 1000 VUs 01.9s/10s
running (02.9s), 1000/1000 VUs, 1977 complete and 0 interrupted iterations
default [ 29% ] 1000 VUs 02.9s/10s
running (03.9s), 1000/1000 VUs, 2971 complete and 0 interrupted iterations
default [ 39% ] 1000 VUs 03.9s/10s
running (04.9s), 1000/1000 VUs, 3963 complete and 0 interrupted iterations
default [ 49% ] 1000 VUs 04.9s/10s
running (05.9s), 1000/1000 VUs, 4960 complete and 0 interrupted iterations
default [ 59% ] 1000 VUs 05.9s/10s
running (06.9s), 1000/1000 VUs, 5958 complete and 0 interrupted iterations
default [ 69% ] 1000 VUs 06.9s/10s
running (07.9s), 1000/1000 VUs, 6958 complete and 0 interrupted iterations
default [ 79% ] 1000 VUs 07.9s/10s
running (08.9s), 1000/1000 VUs, 7958 complete and 0 interrupted iterations
default [ 89% ] 1000 VUs 08.9s/10s
running (09.9s), 1000/1000 VUs, 8958 complete and 0 interrupted iterations
default [ 99% ] 1000 VUs 09.9s/10s
running (10.9s), 0032/1000 VUs, 9954 complete and 0 interrupted iterations
default ↓ [ 100% ] 1000 VUs 10s
█ THRESHOLDS
checks
✓ 'rate>0.9' rate=100.00%
http_req_duration
✓ 'p(95)<1000' p(95)=33.92ms
http_req_failed
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......................: 9986 906.876199/s
checks_succeeded...................: 100.00% 9986 out of 9986
checks_failed......................: 0.00% 0 out of 9986
✓ body check
HTTP
http_req_duration.......................................................: avg=16.58ms min=8.6ms med=12.97ms max=302.7ms p(90)=18.85ms p(95)=33.92ms
{ expected_response:true }............................................: avg=16.58ms min=8.6ms med=12.97ms max=302.7ms p(90)=18.85ms p(95)=33.92ms
http_req_failed.........................................................: 0.00% 0 out of 9986
http_reqs...............................................................: 9986 906.876199/s
EXECUTION
iteration_duration......................................................: avg=1.06s min=1s med=1.01s max=2.28s p(90)=1.22s p(95)=1.52s
iterations..............................................................: 9986 906.876199/s
vus.....................................................................: 32 min=32 max=1000
vus_max.................................................................: 1000 min=1000 max=1000
NETWORK
data_received...........................................................: 9.0 MB 815 kB/s
data_sent...............................................................: 1.7 MB 151 kB/s
running (11.0s), 0000/1000 VUs, 9986 complete and 0 interrupted iterations
default ✓ [ 100% ] 1000 VUs 10s
結果
今回のような簡単な API であれば 1000 並列くらいであれば安定していました
応答速度は実行ごとにばらつきがありましたが平均的には REST API と GraphQL API であまり差はなかったです
また、どちらも API でも安定して 95% 以上の応答が 100ms 以内になっていました
終わりに
開発環境等を用意せずに AWS マネジメントコンソールのみで実装できました
サーバーレス構成で負荷試験が実行できるので、コストを抑えて試しやすいと感じました
参考
k6
サーバーレスで負荷試験!Step Functions + Lambdaを使ったk6の分散実行