k6負荷テスト完全ガイド2026|シナリオ設計・閾値設定・CI/CD連携・結果分析


k6とは

k6はGrafana Labsが開発するオープンソースの負荷テストツールです。JavaScript/TypeScriptでテストシナリオを記述し、Go製のランタイムで高速に実行します。

負荷テストツール比較

ツール言語プロトコルCI/CDクラウド学習コスト
k6JavaScriptHTTP/WS/gRPCGrafana Cloud
JMeterXML/GUIHTTP/JDBC等BlazeMeter
LocustPythonHTTPなし
ArtilleryYAML/JSHTTP/WSArtillery Cloud
GatlingScala/JavaHTTPGatling Enterprise

k6が選ばれる理由

  • JavaScript/TypeScriptでシナリオを書ける(フロントエンド開発者にも親しみやすい)
  • Go製ランタイムで高パフォーマンス(1台で数万VU)
  • CLI完結(GUIツール不要)
  • CI/CD統合が容易(GitHub Actions, GitLab CI等)
  • Grafana連携で結果を美しく可視化

セットアップ

インストール

# macOS
brew install k6

# Linux
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
  --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
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 run - <script.js

# npm(TypeScript対応)
npm install -D @types/k6

プロジェクト構成

load-tests/
├── scenarios/
│   ├── smoke.ts          # スモークテスト
│   ├── load.ts           # 通常負荷テスト
│   ├── stress.ts         # ストレステスト
│   ├── spike.ts          # スパイクテスト
│   └── soak.ts           # 耐久テスト
├── helpers/
│   ├── auth.ts           # 認証ヘルパー
│   └── data.ts           # テストデータ
├── thresholds.json       # 閾値設定
└── package.json

基本的なテストスクリプト

最小構成

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

// テスト設定
export const options = {
  vus: 10,        // 仮想ユーザー数
  duration: '30s', // テスト時間
};

// テストシナリオ
export default function () {
  const res = http.get('https://api.example.com/users');

  // レスポンス検証
  check(res, {
    'ステータス200': (r) => r.status === 200,
    'レスポンス500ms以内': (r) => r.timings.duration < 500,
    'ボディにusersを含む': (r) => r.body.includes('users'),
  });

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

実行

k6 run scenarios/smoke.ts

# 出力例
#          /\      |‾‾| /‾‾/   /‾‾/
#     /\  /  \     |  |/  /   /  /
#    /  \/    \    |     (   /   ‾‾\
#   /          \   |  |\  \ |  (‾)  |
#  / __________ \  |__| \__\ \_____/
#
#   execution: local
#   script: scenarios/smoke.ts
#
#   ✓ ステータス200
#   ✓ レスポンス500ms以内
#
#   http_req_duration.............: avg=120ms  min=80ms  max=250ms
#   http_reqs.....................: 280    9.3/s
#   vus..........................: 10     min=10  max=10

負荷パターン(Scenarios)

スモークテスト(最小負荷で動作確認)

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

通常負荷テスト(段階的にスケールアップ)

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // 2分かけて50VUまで増加
    { duration: '5m', target: 50 },   // 5分間50VUを維持
    { duration: '2m', target: 100 },  // 2分かけて100VUに
    { duration: '5m', target: 100 },  // 5分間100VUを維持
    { duration: '2m', target: 0 },    // 2分かけて終了
  ],
  thresholds: {
    http_req_duration: ['p(95)<1000', 'p(99)<2000'],
    http_req_failed: ['rate<0.05'],
  },
};

ストレステスト(限界を探る)

export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '5m', target: 200 },
    { duration: '2m', target: 300 },  // 通常の3倍
    { duration: '5m', target: 300 },
    { duration: '5m', target: 0 },
  ],
};

スパイクテスト(急激なアクセス増)

export const options = {
  stages: [
    { duration: '10s', target: 100 },  // 10秒で急増
    { duration: '1m', target: 100 },
    { duration: '10s', target: 1000 }, // 10秒で10倍に!
    { duration: '3m', target: 1000 },
    { duration: '10s', target: 100 },  // 急減
    { duration: '3m', target: 100 },
    { duration: '5m', target: 0 },
  ],
};

Scenariosによる複合テスト

export const options = {
  scenarios: {
    // シナリオ1: 一般ユーザーのブラウジング
    browsing: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
      exec: 'browsingScenario',
    },
    // シナリオ2: APIへの書き込みリクエスト
    api_writes: {
      executor: 'constant-arrival-rate',
      rate: 30,          // 1秒あたり30リクエスト
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 50,
      exec: 'apiWriteScenario',
    },
    // シナリオ3: 管理者の重い操作
    admin_operations: {
      executor: 'per-vu-iterations',
      vus: 5,
      iterations: 10,
      exec: 'adminScenario',
      startTime: '1m', // 1分後に開始
    },
  },
};

export function browsingScenario() {
  http.get('https://api.example.com/products');
  sleep(2);
  http.get('https://api.example.com/products/1');
  sleep(1);
}

export function apiWriteScenario() {
  const payload = JSON.stringify({
    title: `Post ${Date.now()}`,
    body: 'テスト投稿の本文です',
  });

  http.post('https://api.example.com/posts', payload, {
    headers: { 'Content-Type': 'application/json' },
  });
}

export function adminScenario() {
  http.get('https://api.example.com/admin/reports');
  sleep(5);
}

閾値(Thresholds)

閾値はテストの合否判定基準です。CI/CDで自動的にパス/フェイルを判断できます。

export const options = {
  thresholds: {
    // レスポンス時間
    http_req_duration: [
      'p(95)<500',    // 95パーセンタイルが500ms以内
      'p(99)<1500',   // 99パーセンタイルが1500ms以内
      'avg<200',      // 平均200ms以内
      'max<3000',     // 最大3000ms以内
    ],

    // エラー率
    http_req_failed: [
      'rate<0.01',    // エラー率1%未満
    ],

    // スループット
    http_reqs: [
      'rate>100',     // 1秒あたり100リクエスト以上
    ],

    // カスタムメトリクス
    'http_req_duration{name:login}': ['p(95)<1000'],
    'http_req_duration{name:search}': ['p(95)<2000'],

    // チェックの成功率
    checks: ['rate>0.99'], // 99%以上成功
  },
};

カスタムメトリクス

import { Trend, Counter, Rate, Gauge } from 'k6/metrics';

// カスタムメトリクスの定義
const loginDuration = new Trend('login_duration');
const loginFailures = new Counter('login_failures');
const loginSuccess = new Rate('login_success');
const activeUsers = new Gauge('active_users');

export default function () {
  const start = Date.now();

  const res = http.post('https://api.example.com/login', JSON.stringify({
    email: 'test@example.com',
    password: 'password123',
  }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { name: 'login' }, // タグ付け
  });

  // カスタムメトリクスに記録
  loginDuration.add(Date.now() - start);

  if (res.status === 200) {
    loginSuccess.add(1);
  } else {
    loginSuccess.add(0);
    loginFailures.add(1);
  }

  activeUsers.add(__VU); // 現在のVU番号
}

実践的なシナリオ

EC サイトの負荷テスト

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

// テストデータ(1回だけ読み込み、全VUで共有)
const users = new SharedArray('users', function () {
  return JSON.parse(open('./data/users.json'));
});

const products = new SharedArray('products', function () {
  return JSON.parse(open('./data/products.json'));
});

const BASE_URL = 'https://api.example.com';

export const options = {
  scenarios: {
    shopping_flow: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },
        { duration: '10m', target: 100 },
        { duration: '2m', target: 0 },
      ],
    },
  },
  thresholds: {
    'group_duration{group:::商品一覧}': ['p(95)<1000'],
    'group_duration{group:::商品詳細}': ['p(95)<500'],
    'group_duration{group:::カート追加}': ['p(95)<800'],
    'group_duration{group:::購入完了}': ['p(95)<3000'],
    http_req_failed: ['rate<0.02'],
  },
};

export default function () {
  const user = users[__VU % users.length];

  // ログイン
  const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
    email: user.email,
    password: user.password,
  }), {
    headers: { 'Content-Type': 'application/json' },
  });

  const token = loginRes.json('token');
  const authHeaders = {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  };

  // 商品一覧を閲覧
  group('商品一覧', function () {
    const res = http.get(`${BASE_URL}/products?page=1&limit=20`, {
      headers: authHeaders,
    });
    check(res, { '商品一覧200': (r) => r.status === 200 });
  });

  sleep(2); // ページを見る

  // 商品詳細を閲覧
  const productId = products[Math.floor(Math.random() * products.length)].id;
  group('商品詳細', function () {
    const res = http.get(`${BASE_URL}/products/${productId}`, {
      headers: authHeaders,
    });
    check(res, { '商品詳細200': (r) => r.status === 200 });
  });

  sleep(3); // 商品を検討

  // カートに追加
  group('カート追加', function () {
    const res = http.post(`${BASE_URL}/cart/items`, JSON.stringify({
      productId: productId,
      quantity: 1,
    }), { headers: authHeaders });
    check(res, { 'カート追加成功': (r) => r.status === 201 });
  });

  sleep(1);

  // 購入完了(10%のユーザーのみ)
  if (Math.random() < 0.1) {
    group('購入完了', function () {
      const res = http.post(`${BASE_URL}/orders`, JSON.stringify({
        paymentMethod: 'credit_card',
      }), { headers: authHeaders });
      check(res, { '購入成功': (r) => r.status === 201 });
    });
  }

  sleep(1);
}

認証付きAPIテスト

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

// setup: テスト開始前に1回だけ実行
export function setup() {
  const res = http.post('https://api.example.com/auth/login', JSON.stringify({
    email: 'admin@example.com',
    password: 'admin-password',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });

  return { token: res.json('token') };
}

// default: 各VUが繰り返し実行
export default function (data) {
  const res = http.get('https://api.example.com/protected/resource', {
    headers: {
      Authorization: `Bearer ${data.token}`,
    },
  });

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

// teardown: テスト終了後に1回だけ実行
export function teardown(data) {
  http.post('https://api.example.com/auth/logout', null, {
    headers: {
      Authorization: `Bearer ${data.token}`,
    },
  });
}

gRPC / WebSocketテスト

gRPCテスト

import grpc from 'k6/net/grpc';
import { check } from 'k6';

const client = new grpc.Client();
client.load(['definitions'], 'user_service.proto');

export default function () {
  client.connect('grpc.example.com:443', { tls: true });

  const response = client.invoke('user.UserService/GetUser', {
    id: '123',
  });

  check(response, {
    'gRPCステータスOK': (r) => r.status === grpc.StatusOK,
    'ユーザー名が存在': (r) => r.message.name !== '',
  });

  client.close();
}

WebSocketテスト

import ws from 'k6/ws';
import { check } from 'k6';

export default function () {
  const url = 'wss://ws.example.com/chat';

  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', function () {
      socket.send(JSON.stringify({
        type: 'join',
        room: 'general',
      }));

      // 5秒ごとにメッセージ送信
      socket.setInterval(function () {
        socket.send(JSON.stringify({
          type: 'message',
          text: `Hello from VU ${__VU}`,
        }));
      }, 5000);
    });

    socket.on('message', function (msg) {
      const data = JSON.parse(msg);
      check(data, {
        'メッセージタイプが正しい': (d) => ['join', 'message'].includes(d.type),
      });
    });

    socket.on('error', function (e) {
      console.error('WebSocketエラー:', e.error());
    });

    // 30秒後に切断
    socket.setTimeout(function () {
      socket.close();
    }, 30000);
  });

  check(res, {
    'WebSocket接続成功': (r) => r && r.status === 101,
  });
}

CI/CD連携

GitHub Actions

# .github/workflows/load-test.yml
name: Load Test

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1' # 毎週月曜6:00

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

      - name: k6をインストール
        uses: grafana/setup-k6-action@v1

      - name: スモークテスト実行
        run: k6 run scenarios/smoke.ts

      - name: 負荷テスト実行
        run: |
          k6 run \
            --out json=results.json \
            scenarios/load.ts

      - name: 結果をアーティファクトとして保存
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: k6-results
          path: results.json

閾値でCI/CDを制御

// 閾値を超えると exit code 99 でk6が終了
// → CI/CDのステップが失敗扱いになる
export const options = {
  thresholds: {
    http_req_duration: [{
      threshold: 'p(95)<500',
      abortOnFail: true,      // 閾値超過で即中断
      delayAbortEval: '10s',  // 10秒後に評価開始
    }],
    http_req_failed: [{
      threshold: 'rate<0.01',
      abortOnFail: true,
    }],
  },
};

Grafanaダッシュボードで結果を可視化

InfluxDB + Grafana

# InfluxDBに結果を送信
k6 run --out influxdb=http://localhost:8086/k6 scenarios/load.ts

Prometheus + Grafana

# Prometheus Remote Write
k6 run \
  --out experimental-prometheus-rw \
  -e K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \
  scenarios/load.ts

Grafana Cloud k6

# Grafana Cloudに直接送信
k6 cloud scenarios/load.ts

ベストプラクティス

1. テストデータの管理

import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';

// CSVファイルからテストデータを読み込み
const csvData = new SharedArray('csv', function () {
  return papaparse.parse(open('./data/users.csv'), {
    header: true,
  }).data;
});

export default function () {
  // VUごとに異なるデータを使用
  const user = csvData[__VU % csvData.length];
  // ...
}

2. 段階的なテスト戦略

1. スモークテスト(1VU/1分)  → デプロイ直後に毎回
2. 通常負荷テスト(想定負荷)  → リリース前に毎回
3. ストレステスト(想定の2-3倍)→ 月次で実施
4. スパイクテスト(急激な負荷) → セール前に実施
5. 耐久テスト(長時間負荷)    → 四半期ごと

3. 現実的なシナリオ設計

// ❌ 悪い例: 同じエンドポイントを連続で叩く
export default function () {
  http.get('/api/products');
  http.get('/api/products');
  http.get('/api/products');
}

// ✅ 良い例: 実際のユーザー行動を模倣
export default function () {
  http.get('/api/products');          // 一覧を見る
  sleep(randomIntBetween(1, 5));     // 考える時間
  http.get('/api/products/123');      // 詳細を見る
  sleep(randomIntBetween(2, 8));     // 検討する時間
  http.post('/api/cart', payload);   // カートに入れる
  sleep(randomIntBetween(1, 3));
}

まとめ

テストタイプVU数目的実施頻度
スモーク1-5基本動作確認毎デプロイ
負荷想定ピークSLA検証毎リリース
ストレス想定の2-3倍限界値把握月次
スパイク急激に増減回復力検証イベント前
耐久中負荷長時間メモリリーク検出四半期

k6の強みは開発者フレンドリーな点です。JavaScriptでシナリオを書き、CLI一本で実行、閾値でCI/CDを自動判定、Grafanaで美しく可視化——このワークフローで、パフォーマンスの問題をリリース前に確実に検出できます。

関連記事