Distributed Load Testing on AWS でスクリプト不要の負荷テストを試してみた
こんにちは。製造ビジネステクノロジー部の小林です。
システムの非機能要件を検証する際、負荷テストを実施することがあると思います。しかし、負荷テスト専用の環境を構築するには様々なコストがかかりますよね。
AWS Distributed Load Testing on AWS(以降、DLTと呼称します) を利用すると、負荷テストを実施する際の負荷生成用インフラを簡単に構築できます。
今回はこのDLTでk6を利用して、AWSのサーバーレスアーキテクチャへの負荷テストを実施してみました。
AWS Distributed Load Testing on AWS(DLT) とは?
「Distributed Load Testing on AWS」は、 HTTP または HTTPS のエンドポイントに対して、数万の同時接続をシミュレートし、負荷テストを実行することが可能なソリューションです。本ソリューションをデプロイ後、 Amazon S3 にてホストされる Web コンソールにアクセスし、接続先エンドポイント、同時接続数、テスト時間などを指定することでテストを実行できます。
DLTは、AWS が提供する公式のCloudFormationテンプレートを使用して簡単にデプロイできます。内部的には Amazon ECS (Fargate) を使用して負荷テストタスクを並列実行し、分散環境から大規模な負荷をかけることができます。
DLTの特徴
- 簡単なデプロイ: CloudFormationテンプレートで数分でセットアップ完了
- スケーラブル: 複数のFargateタスクで負荷を分散実行
- Web UI: ブラウザから簡単にテスト設定・実行・結果確認
- 複数のツールに対応: JMeter、k6、Locustなどのスクリプトをサポート
k6 とは?
k6 は、Grafana Labs が開発したオープンソースの負荷テストツールです。JavaScript/TypeScriptでテストシナリオを記述できます。
今回は、DLTでk6スクリプトを実行して負荷テストを試してみます。
検証環境の構成
今回は、以下のような構成で負荷テストを実施します。

DLTの詳細については、こちらの記事もご覧ください。
DLT の構築
それでは実際に負荷テストを行う環境を構築していきます。
まず、AWS公式のDLTソリューションをデプロイします。下記の公式ドキュメントの手順通りに進めます。
途中でDLTを既存VPCに作成するかを選択する項目があります。今回は新しいVPCを作成します。

初回アクセス時に、管理者メールアドレスに送信された一時パスワードを使用してログインします。ログインすると下記のコンソールが表示されます。

Fargateのサービスクォータについて
DLTを使用する際、AWS Fargateのサービスクォータに注意が必要です。
デフォルトのクォータ
DLTでは、デフォルトで2 vCPUのFargateタスクを使用します。各タスクが負荷テストを実行するため、実行できるタスク数はアカウントのFargateクォータに依存します。
現在のアカウントのFargate On-Demand vCPU resource countは400 vCPUです。

つまり、最大200タスク(400 vCPU ÷ 2 vCPU/タスク)を同時実行できます。
タスクあたりの推奨VU数
公式ドキュメントによると、1タスクあたりの推奨同時ユーザー数は200 VUとされています。
デフォルト設定にもとづく同時ユーザーの推奨制限は 200 ユーザーです。
これらを踏まえると、現在のクォータでは理論上最大40,000 VU(200タスク × 200 VU/タスク)まで実行可能です。ただし、実際には今回のテスト対象であるLambda関数の同時実行数(960)が先にボトルネックになるため、負荷テストの規模はLambda側の制約に合わせて実施しました。
テストの実行
それではテストを実行していきましょう。DLTのコンソールで「テストシナリオを作成する」をクリックします。

名前: テストシナリオの名前を入力
詳細: テストの目的や内容を記載
タグは空欄のままで構いません。「今すぐ実行」にチェックを入れて、次に進みます。

テストタイプで「k6」にチェックを入れ、事前に準備したk6スクリプトファイル(.js)をアップロードします。

ここで少しハマりました...
k6スクリプトとDLTの役割
事前に、k6で詳細なテストシナリオ(段階的な負荷増加、複数のシナリオ、閾値など)を定義したスクリプトを作成していました。
しかし、DLTのWeb UIを見てみると、「タスク数」「同時実行数」「Ramp Up(増加時間)」「Hold For(維持時間)」といった負荷パターンの設定項目があることに気づきました。


つまり、負荷パターンの制御はDLT側で行うため、k6スクリプト内で複雑なシナリオを定義する必要はなかったのです。
事前に作成したk6のスクリプト
import http from 'k6/http';
import { check, sleep } from 'k6';
// テスト対象のAPI URL(環境変数から取得、またはデフォルト値を使用)
const API_URL = __ENV.API_URL || 'https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1';
/**
* K6テストオプション設定
*
* テスト対象: GET /users(ユーザー一覧取得API)
*
* システム制約:
* - Lambda予約済み同時実行数: 960
* - 想定レスポンスタイム: 300ms
* - 理論上の最大RPS: 960 ÷ 0.3秒 ≈ 3,200 RPS
*
* テストの目的:
* 1. 本番想定負荷(1,800 RPS)での安定性確認
* 2. Lambda同時実行数制限でのスロットル挙動確認
* 3. システムの限界性能測定
*/
export const options = {
scenarios: {
/**
* シナリオ1: 同時接続数テスト
*
* 目的: 一定数の同時接続を維持できるか確認
* 実行時間: 0分〜2分
* VU数: 500(仮想ユーザー数)
* 期待される挙動: 安定したレスポンスタイムを維持
*/
constant_vus: {
executor: 'constant-vus', // 一定のVU数を維持
vus: 500, // 500同時接続
duration: '2m', // 2分間実行
startTime: '0s', // テスト開始直後から実行
},
/**
* シナリオ2: 目標RPSテスト
*
* 目的: 本番想定の1,800 RPSを維持できるか確認
* 実行時間: 2分〜5分
* RPS: 1,800リクエスト/秒
* 期待される挙動: エラー率2%未満、P95 < 500ms
*/
target_rps: {
executor: 'constant-arrival-rate', // 一定のRPSを維持
rate: 1800, // 1,800リクエスト/秒
timeUnit: '1s', // 1秒あたり
duration: '3m', // 3分間実行
preAllocatedVUs: 100, // 事前に割り当てるVU数
maxVUs: 2000, // 最大VU数(負荷に応じて自動調整)
startTime: '2m', // シナリオ1終了後に開始
},
/**
* シナリオ3: 限界性能テスト(段階的負荷増加)
*
* 目的: システムの限界RPSとボトルネックを特定
* 実行時間: 5分〜12分
* RPS: 100 → 3,500(段階的に増加)
*
* 期待される挙動:
* - 3,200 RPS付近でLambda同時実行数が960に到達
* - 3,500 RPSでスロットル(429エラー)が発生
*/
ramping_rps: {
executor: 'ramping-arrival-rate', // RPSを段階的に増加
startRate: 100, // 開始時のRPS
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 3000, // 最大3,000 VU(Lambda制約に合わせる)
stages: [
{ duration: '1m', target: 500 }, // 1分で500 RPSまで増加
{ duration: '1m', target: 1000 }, // 1分で1,000 RPSまで増加
{ duration: '1m', target: 1800 }, // 1分で1,800 RPSまで増加(目標値)
{ duration: '1m', target: 2500 }, // 1分で2,500 RPSまで増加
{ duration: '1m', target: 3200 }, // 1分で3,200 RPSまで増加(Lambda理論上の限界)
{ duration: '1m', target: 3500 }, // 1分で3,500 RPSまで増加(スロットル発生を確認)
{ duration: '1m', target: 0 }, // 1分でクールダウン
],
startTime: '5m', // シナリオ2終了後に開始
},
},
/**
* 閾値(Thresholds)設定
*
* これらの条件を満たさない場合、テストは失敗とみなされます。
*
* - http_req_duration: レスポンスタイム
* - P95(95パーセンタイル): 500ms以下
* - P99(99パーセンタイル): 1,000ms以下
*
* - http_req_failed: HTTPリクエスト失敗率
* - 2%未満(Lambda スロットル時は増加する)
*/
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.02'],
},
};
/**
* メインテスト関数
*
* 各仮想ユーザー(VU)が繰り返し実行する処理。
* k6がシナリオ設定に従って、この関数を並列実行します。
*/
export default function () {
// GET /users リクエストを送信(ユーザー一覧取得)
const res = http.get(`${API_URL}/users`);
/**
* レスポンスの検証
*
* check関数で複数の条件をチェックし、結果をk6のメトリクスに記録します。
*
* チェック項目:
* 1. ステータスコードが200(正常)
* 2. ステータスコードが429でない(スロットルされていない)
* 3. レスポンスタイムが500ms未満
*/
check(res, {
'status is 200': (r) => r.status === 200,
'status is not 429 (throttled)': (r) => r.status !== 429,
'response time < 500ms': (r) => r.timings.duration < 500,
});
/**
* 次のリクエストまで0.1秒待機
*/
sleep(0.1);
}
そのため、今回はk6のスクリプトファイルは使用せず、DLTのみで負荷テストを行うことにしました。
テストの実行
それでは、改めて最初からシナリオの作成をしていきます。今回は「Single HTTP Endpoint」にチェックを入れて、API GatewayのエンドポイントURLを直接指定します。

負荷パラメータは以下のように設定しました。
タスク数: 4
同時実行数: 200
つまり、最大同時実行数は800(4タスク × 200VU)となります。

Ramp Up(目標の同時実行数に到達するまでの時間): 10秒
持続時間(最大負荷を維持する時間)を1分にしました。

設定が完了したので、テストを実行します。

テスト実行中の動作
テスト実行中は、リアルタイムでメトリクスを確認できます。

この画面は文字通り「リアルタイム」専用であり、テスト実行中のみ表示されます。テスト終了後は閲覧できなくなるため、必要に応じてスクリーンショットを取得しておきましょう。

テスト終了後は、詳細なレポートをダウンロードできます。

無事負荷テストが実行できました。
おわりに
今回は、AWS Distributed Load Testing on AWS(DLT)を使用して、サーバーレスアーキテクチャに対する負荷テストを実施しました。
CloudFormationテンプレートで数分でデプロイでき、準備時間を大幅に削減できました!
Web UIから設定値を入力するだけでテストを実行でき、スクリプトの記述が不要な点もよかったです。
当初はk6でシナリオを作成していましたが、今回のように単純なHTTPエンドポイントのテストであれば、DLTのWeb UIだけで完結でき、工数を削減できますね。
一方で、認証が必要なAPIのテスト(例: ログイン → トークン取得 → 認証付きAPIリクエスト)など、複雑なシナリオが必要な場合は、k6などのスクリプトファイルを利用したテストが適していそうです。この記事が皆さんの参考になれば幸いです。
参考リンク










