0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

k6×GitHub Actionsで「本番で遅い」を未然に防ぐ——負荷テスト自動化ハンズオン入門

0
Posted at

この記事で分かること

  • 負荷テストとは何か、なぜ「リリース前」に自動化すべきなのか
  • k6のインストールから最初の負荷テスト実行まで
  • テストシナリオの書き方(しきい値・チェック・段階的負荷)
  • GitHub Actionsに組み込んでPRごとに自動実行する具体的な手順
  • よくあるハマりどころと対処法

はじめに

「テスト環境では問題なかったのに、本番にデプロイしたらレスポンスが遅い」「セール開始直後にサーバーが落ちた」——こうした問題は、コードの機能テスト(単体テスト・E2Eテスト)だけでは検出できません。

原因は負荷をかけた状態でのパフォーマンスを事前に確認していないことにあります。手動で負荷テストを実施するチームもありますが、リリースのたびに人手で実行するのは現実的ではなく、やがて省略されがちです。

この記事では、k6(ケーシックス)という負荷テストツールとGitHub Actionsを組み合わせて、「PRを出すたびに自動で負荷テストが走り、基準を満たさなければマージできない」仕組みを構築します。

前提知識

  • 負荷テスト: 多数のユーザーが同時にアクセスする状況をシミュレーションし、システムの応答速度やエラー率を測定するテストです。レストランに例えると「開店前にスタッフ総出でお客さん役をやり、オペレーションが回るか確認する」ようなものです
  • VU(Virtual User): k6が生成する仮想的なユーザーのことです。VU数を増やすほど、同時アクセスが増えます
  • p(95): レスポンスタイムの95パーセンタイル値です。「100回リクエストしたら95回はこの時間以内に返る」という意味で、極端に遅い外れ値に引っ張られにくい指標です
  • GitHub Actions: GitHubリポジトリにプッシュやPR作成などのイベントが発生した際に、自動でスクリプトを実行する仕組みです

なぜk6を選ぶのか

負荷テストツールには JMeter、Gatling、Locust など多くの選択肢があります。k6を選ぶ理由は以下の通りです。

観点 k6 JMeter Gatling
テスト定義 JavaScript(コードで書く) GUI / XML Scala / Java
CI/CD親和性 CLI実行、終了コードで判定 JARファイルで実行可能 sbt/Mavenで実行
学習コスト JSが書ければすぐ開始 GUIだが概念が多い Scalaの知識が必要
リソース消費 Go製で軽量 JVM、メモリ消費大 JVM、中程度
公式CI連携 grafana/k6-action なし(自前構築) 公式プラグインあり

k6はJavaScriptでテストシナリオを書けるため、フロントエンド・バックエンドを問わず多くのエンジニアが馴染みやすく、Go製のバイナリ1つで動作するため、CIランナー上でも軽量に実行できます。

負荷テストの種類を知る

k6では目的に応じて異なるパターンの負荷をかけます。すべてを最初から実施する必要はなく、まずはSmokeテストから始めるのがおすすめです。

パターン 知りたいこと 負荷の形 いつやるか
Smoke そもそも動くか 1〜2VU、数回 CI/CDで毎回
Load 通常アクセスで大丈夫か 台形(段階的に上下) スプリントごと
Stress どこまで耐えるか 階段(段階的に上げる) リリース前
Spike 急増に耐えるか 針(急増→急減) スケーリング検証
Soak 長時間で劣化しないか 一定負荷を長時間 定期的に
Breakpoint 何人で壊れるか 上げ続ける キャパシティ計画時

ハンズオン:k6で最初の負荷テストを書く

Step 1: k6のインストール

# macOS
brew install k6

# Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
  --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6

# Docker
docker run --rm -i grafana/k6 version

Step 2: 最初のテストスクリプト

load-tests/smoke.js を作成します。

import http from 'k6/http';
import { check, sleep } from 'k6';

// テストの設定
export const options = {
  // Smokeテスト: 1VUで10回実行
  vus: 1,
  iterations: 10,

  // しきい値: これを超えたらテスト失敗
  thresholds: {
    http_req_failed: ['rate<0.05'],        // エラー率5%未満
    http_req_duration: ['p(95)<500'],       // 95%のリクエストが500ms以内
  },
};

export default function () {
  // テスト対象のAPIを呼び出す
  const res = http.get('https://test-api.k6.io/public/crocodiles/');

  // レスポンスの検証
  check(res, {
    'ステータスが200': (r) => r.status === 200,
    '200ms以内に応答': (r) => r.timings.duration < 200,
  });

  // 実際のユーザー行動を模倣して待機
  sleep(1);
}

このスクリプトのポイントを解説します。

  • options: テストの実行条件を定義します。thresholds(しきい値)を設定しておくと、基準を満たさない場合にk6がゼロ以外の終了コードを返します。これがCI連携の要です
  • default function: VU(仮想ユーザー)が繰り返し実行する処理です。1回の実行を「イテレーション」と呼びます
  • check(): レスポンスの内容を検証します。ただし、check() が失敗してもテストは続行されます。テスト自体を失敗させたい場合は thresholds で制御します
  • sleep(): 実際のユーザーはページを読んだりフォームを入力したりするため、リクエスト間に待機時間を入れます。これを入れ忘れると、現実離れした負荷がかかります

Step 3: テスト実行

k6 run load-tests/smoke.js

実行結果には以下のようなメトリクスが表示されます。

     ✓ ステータスが200
     ✓ 200ms以内に応答

     checks.........................: 100.00% ✓ 20  ✗ 0
     http_req_duration..............: avg=45.2ms  min=38.1ms  max=62.3ms  p(95)=58.7ms
     http_req_failed................: 0.00%   ✓ 0   ✗ 10

主要メトリクスの読み方

k6が出力するメトリクスのうち、特に重要なものを押さえておきましょう。

メトリクス 意味 注目すべき値
http_req_duration リクエスト送信〜レスポンス完了の時間 p(95), avg
http_req_failed 4xx/5xxの割合 0に近いほど良い
http_req_waiting TTFB(サーバーの処理時間) サーバー側のボトルネック指標
http_req_blocked TCP接続待ちの時間 コネクションプール不足の兆候
checks check() の成功率 100%が理想

Step 4: 段階的負荷テスト(Load Test)に進化させる

Smokeテストが通ったら、実際の利用に近い負荷をかけるLoadテストを書きます。

load-tests/load.js を作成します。

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  // 段階的に負荷を変化させる
  stages: [
    { duration: '1m', target: 10 },  // 1分かけて10VUまで増加
    { duration: '3m', target: 10 },  // 3分間10VUを維持
    { duration: '1m', target: 0 },   // 1分かけて0VUに減少
  ],

  thresholds: {
    http_req_failed: ['rate<0.05'],
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
  },
};

export default function () {
  const res = http.get('https://test-api.k6.io/public/crocodiles/');

  check(res, {
    'ステータスが200': (r) => r.status === 200,
    'レスポンスボディが存在': (r) => r.body && r.body.length > 0,
  });

  // ユーザーのページ閲覧時間を模倣(1〜3秒)
  sleep(Math.random() * 2 + 1);
}

stages を使うと「徐々にユーザーが増えて、一定時間利用して、徐々にいなくなる」という現実に近いパターンを再現できます。これは台形のような負荷曲線になります。

しきい値の設計指針

テストパターンによって適切なしきい値は異なります。高負荷テストでエラーや遅延が出るのは当然なので、しきい値を厳しくしすぎるとテストの目的を見失います。

テストパターン エラー率 p(95) 考え方
Load Test <0.05(5%) <500ms プロダクトの非機能要件そのもの
Stress Test <0.1(10%) <2000ms 高負荷なので緩め。回復できるかが重要
Spike Test <0.15(15%) <5000ms 急増時のエラーは許容。回復速度を見る

GitHub Actionsで自動実行する

ここからが本記事の核心です。PRを出すたびにk6が自動実行される仕組みを作ります。

Step 5: ワークフローファイルの作成

.github/workflows/k6-load-test.yml を作成します。

name: k6 Load Tests

on:
  pull_request:
    branches: [main]

jobs:
  smoke-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run k6 Smoke Test
        uses: grafana/k6-action@v0.3.0
        with:
          filename: load-tests/smoke.js
        env:
          BASE_URL: ${{ secrets.STAGING_API_URL }}

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: k6-results
          path: results/
          retention-days: 30

このワークフローのポイントは以下の通りです。

  • grafana/k6-action: Grafana公式のGitHub Actionで、k6のインストールと実行を1ステップで行えます
  • on: pull_request: PRが作成・更新されたタイミングで自動実行されます
  • secrets.STAGING_API_URL: テスト対象のURLをGitHubのSecretsに登録しておくことで、リポジトリに直接URLを書かずに済みます
  • k6の thresholds を超えた場合、k6はゼロ以外の終了コードを返し、ワークフローが自動的にFailになります

Step 6: 環境変数をスクリプトで使う

テスト対象のURLをハードコードせず、環境変数で切り替えられるようにします。

// load-tests/smoke.js(環境変数対応版)
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export const options = {
  vus: 1,
  iterations: 10,
  thresholds: {
    http_req_failed: ['rate<0.05'],
    http_req_duration: ['p(95)<500'],
  },
};

export default function () {
  const res = http.get(`${BASE_URL}/api/health`);

  check(res, {
    'ステータスが200': (r) => r.status === 200,
  });

  sleep(1);
}

__ENV.BASE_URL で環境変数を参照できます。ローカル開発時は http://localhost:3000、CI環境では secrets.STAGING_API_URL が使われる仕組みです。

Step 7: ブランチ保護ルールで品質を強制する

GitHub Actionsが動くだけでは「テストが落ちてもマージできてしまう」状態です。これを防ぐために、ブランチ保護ルールを設定します。

  1. GitHubリポジトリの Settings → Branches → Branch protection rules を開く
  2. main ブランチに対してルールを追加
  3. Require status checks to pass before merging にチェック
  4. Status checkssmoke-test(ジョブ名)を追加

これで、k6のしきい値を満たさないPRはマージできなくなります。

よくある落とし穴と対処法

k6を導入する際にハマりやすいポイントをまとめます。

1. sleep() を入れ忘れて非現実的な負荷になる

sleep() なしだと、VUはレスポンスを受け取った瞬間に次のリクエストを送ります。実際のユーザーはページを読んだり考えたりする時間があるので、ページ閲覧なら1〜3秒、フォーム入力なら5〜10秒の待機を入れましょう。

2. check() の失敗でテストが止まると思い込む

check() は検証結果を記録するだけで、テストは続行されます。テストを失敗させるには thresholdschecks: ['rate>0.99'] のように設定します。

3. setup() の戻り値に関数を入れてしまう

setup() から default function へのデータ受け渡しはJSONシリアライズ経由で行われます。関数やDBコネクションは渡せません。トークン文字列や設定値などのプリミティブ値・オブジェクトのみ使用できます。

4. Docker実行時に --network=host を忘れる

ローカルのサーバーに対してDocker内のk6からテストする場合、--network=host を付けないと localhost にアクセスできません。

docker run --rm --network=host -i grafana/k6 run - < load-tests/smoke.js

5. CIでの負荷テストが本番に影響する

CI環境からはステージング環境にのみ負荷をかけてください。 本番環境に対してCIから負荷テストを実行すると、実ユーザーに影響を与えます。ステージング環境のURLを secrets で管理し、本番URLが間違って使われないようにしましょう。

まとめ

本記事では、k6によるの負荷テストの基礎から、GitHub Actionsでの自動化までをハンズオン形式で解説しました。

導入のステップをまとめます。

  1. Smokeテストから始める: 1〜2VUで「そもそも動くか」を確認するテストをCIに組み込む
  2. しきい値を設定する: thresholds でp(95)やエラー率の基準を定義する
  3. GitHub Actionsで自動実行: PRごとにテストが走り、基準を満たさなければマージできない状態にする
  4. 段階的にテストを拡充する: Load Test → Stress Test と、必要に応じてテストパターンを増やす

負荷テストは「やらなくても動く」ものですが、「やっておけば本番障害を防げた」ケースは数え切れません。まずは最小のSmokeテスト1本をCIに組み込むところから始めてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?