BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

継続的な負荷テスト環境をBASEに構築しました 〜 第2回: 負荷生成ツールの構築と運用

はじめに

こんにちは、Checkout Reliabilityチームでバックエンドエンジニアをしているかがの(@ykagano)です!

こちらは、「継続的な負荷テスト環境をBASEに構築しました」の第2回の記事です。

先に第1回を読んでいただくのをおすすめします。

継続的な負荷テスト環境をBASEに構築しました 〜 第1回: 負荷テストの全体像 - BASEプロダクトチームブログ

こちらは第1回で紹介したBASEの負荷テスト環境の構成図です。

cart-load-test-system
負荷テスト環境の構成図

本記事では、負荷テストツールとして採用したLocustの選定理由から、実際の構築・運用方法までを紹介します。

負荷生成ツールの選定

負荷テストのリクエスト元となる負荷生成ツールの選定を行いました。

選定の際は、まず今回の要件として以下の通りまとめました。

  • OSSである
    • クラウドはコストとセキュリティの両面でハードルが上がるため
  • 分散実行が可能である
    • 継続的負荷テスト環境としてスケーリングは必要
  • Web UIがある
    • レポート品質が高い
  • 学習コストが低い
    • メンバーの誰もが利用できるようにしたい
  • 実績が豊富である
    • OSSが今後も保守される可能性が高い

こうした観点から参考として整理したOSSの比較表が以下となります。

ツール名 分散実行可能 Web UI レポート品質 学習コスト 実績が豊富 特徴
Locust ◎(Master/Worker が標準) ○ Web UI OSSで最も簡単にクラスタ構成可能。Pythonでシナリオが柔軟。中規模負荷に強い。
Taurus ○(バックエンドの Locust/JMeter/k6 を分散実行管理) ○(バックエンド依存) 低〜中 CI/CDで分散テスト統合運用。YAMLでテストランナー統一。チーム利用に最適。
JMeter ○(分散実行あり、構成は複雑) △(要プラグイン) ◎ HTMLレポート GUI派に最適。歴史が長く安定。大規模負荷にも実績豊富。
k6 OSS △(K8s/CIで水平スケール。自動クラスタなし) △(外部連携で補完) 低〜中 K8sベースのスケール前提なら実質分散可能。軽量で高性能。
Gatling △(OSS版は限定的。分散は Gatling Enterprise 推奨) ◎ HTMLレポート 中〜高 Scala/Java DSLで高精度なシナリオ記述。レポートが非常に詳細。高スループット計測に強い。

k6、JMeter、Locustは自身で使ったことがありました。

k6は軽量ですが、Web UIがなく、分散環境で実行した場合、サーバーごとに出力されるレポートの収集が必要なのが懸念でした。

JMeterはテストシナリオを作るための構成が独特で理解に時間がかかるため、学習コストが高いと感じていました。

LocustはWeb UIから容易に実行でき、Pythonでテストシナリオを書く体験が良かったので、今回もLocustを選定することにしました。

Locustの公式ドキュメントはこちらを参照ください。

https://locust.io/

Locustの構築と実行

ECSに構築しました。ECSはAWSのコンテナ管理サービスです。

LocustはMasterとWorkerのタスクに分かれて起動します。

MasterはWeb UIとWorkerの管理を行い、Workerが実際の負荷をリクエストします。

Workerの数を増やすことで、負荷リクエストの上限も増やすことができる構成です。

locust-system-in-ecs
ECSでのLocustの構成

Classごとに実行が可能で、ProfileでGold環境とBronze環境を切り替えられるようにしています。

locust-execution-page
Locustの実行画面

実行結果はWeb UIからRPS、Response Times、User数がチャートで見れます。

locust-result-page
Locustの実行結果

実際に負荷テストを実行する際は、急激な負荷増加によるスパイクを避けつつ安定状態を観測するため、30秒程度で全てのUserがRamp upするようにして5分間の負荷をかけています。

そして5分の間、応答速度が安定的であるかどうかチャートを見ながら確認しつつ負荷テストを行っています。

Terraformを用いたインフラのコード化

Locustを配置している負荷テスト環境のインフラはTerraformで管理しています。

Terraformはインフラの構成を「設定ファイル」として記述できるOSSです。

AWSコンソールから設定せず、Terraformを用いてコード管理することで以下のメリットがあります。

  • 負荷テスト環境で使用しているリソースをコードから確認できる
  • コードベースでAIに環境構築を依頼できるため、構築が早い
  • 環境の起動と破棄がGitHub Actions(GHA)から実行できる

これにより、負荷テスト環境を使用する時だけ起動し、使い終わったら破棄するといったこともできます。

しかし、負荷テスト環境の構築を始めた当初、チーム内にTerraformを使用したことのある知見がなかったため、メンバーから提案をいただき、まずAIにAWSの基礎知識を含めた学習用コンテンツを作ってもらうことから始めました。

AIは主にClaude Codeを使用しています。

こちらが学習コンテンツの一部ですが、このような資料を作っていました。

learning-content
学習コンテンツ

また環境構築自体も、Claude Codeに設計書を書いてもらい、チーム内でレビューをし、何度も修正を繰り返した上で、Claude CodeにTerraformのコードに落とし込んでもらうという形で進めました。

作成されたコードのレビューはチーム全員で行い、全員が負荷テスト環境について知識を揃えるようにしました。

また今回Terraformのフォルダ構成として、以下の通りcoreとruntimeという二つのフォルダに分けています。

  • core:常設リソースの配置場所で、destroy不可に設定して常設リソースを保護します
  • runtime:一時的なリソースの配置場所で、destroy可能とすることでコストを削減します

これにより、インフラの起動時間の短縮とコストの削減を両立させることができます。

今回はTerraformの構成についての知見も得ることができ、良い経験になりました。

負荷テストシナリオの作成

LocustではPythonでテストシナリオを書くことができます。

下記はClaude Codeに書いてもらったサンプルコードからの抜粋ですが、コードの組み立て方は実コードとあまり変わらないため、イメージしやすいかと思います。

"""Locust による会員登録フローの負荷テストシナリオ"""

import json
import os
import random
import string

from locust import HttpUser, between, task

DEBUG = os.getenv("DEBUG") == "1"

def random_email():
    """テスト用のユニークなメールアドレスを生成する"""
    rand = "".join(random.choices(string.ascii_lowercase + string.digits, k=10))
    return f"test_{rand}@example.com"

def log_response(label, response):
    if not DEBUG:
        return
    code = response.status_code
    mark = "✓" if 200 <= code < 300 else "✗"
    print(f"  {mark} [{code}] {label}")
    if code >= 400:
        print(f"    body: {response.text[:200]}")

class UserRegistration(HttpUser):
    """新規会員登録フローのシナリオ"""

    wait_time = between(1, 3)

    @task
    def register(self):
        email = random_email()

        # 1. 仮登録(メール送信リクエスト)
        with self.client.post(
            "/api/v1/signup",
            json={"email": email},
            catch_response=True,
        ) as r:
            log_response("signup", r)
            if r.status_code != 200:
                r.failure(f"signup: {r.status_code}")
                return
            token = r.json().get("confirmation_token")
            if not token:
                r.failure("signup: token missing")
                return
# この後の処理は省略

今回、テストシナリオを商品や決済方法毎にいくつか作成しました。

作成方法としては過去にk6のJavaScriptで書かれたテストシナリオを流用し、Claude CodeにPythonのコードに変換してもらいました。

変換後のコードが正しいかどうかを検証するために、Playwrightを使用しました。

Playwrightはブラウザを自動操作できるOSSのテストツールです。

Claude CodeをPlaywright MCP(MCPは外部ツールと連携するための共通規格)に接続して、開発環境での購入画面に実際にアクセスしながら、通信しているAPIを確認し、APIコールがテストシナリオのコードと一致しているか検証する形で行いました。

実際に指示したプロンプト

Playwright MCPで下記URLにアクセスして、
購入フローをトレースしながらコールされているAPIが scenario/*****.py の
シナリオに記載されている内容と一致しているかチェックしてください

https://*****/items/12939562

こうしたClaude Codeを用いたテストシナリオの作成により、作業時間を大幅に短縮することができました。

Locustの週次での自動実行

GHAから自動実行する際は、ECSでLocustを起動した後、起動したLocustに対してLambda経由でLocustの負荷テスト実行APIを叩くようにしています。

そして負荷テストの完了結果はログ収集サービスであるCloudWatch Logsをポーリングして、結果ログを取得するようにしています。

以下がそのシーケンス図になります。

sequenceDiagram
    participant GHA as GitHub Actions
    participant Lambda as Lambda<br/>(locust-control)
    participant CWL as CloudWatch Logs
    participant Master as Locust Master<br/>(ECS)
    participant Worker as Locust Worker<br/>(ECS x N)

    Note over GHA: Locust Master/Worker の<br/>デプロイ完了後

    GHA->>Lambda: Lambda を非同期で呼び出す<br/>(ユーザー数・実行時間等を指定)
    Lambda-->>GHA: 即時応答(202)

    Note over GHA: CloudWatch Logs のポーリングを開始<br/>(30秒間隔で結果ログを監視)

    Lambda->>Master: Worker の接続状況を確認<br/>(10秒間隔・最大120秒)
    Master-->>Lambda: 接続済み Worker 一覧

    Lambda->>Master: テスト開始を指示<br/>(POST /swarm)
    Master-->>Lambda: 開始成功

    Master->>Worker: テストシナリオを配信
    Note over Worker: 負荷テスト実行中...

    Note over Lambda: 指定時間が経過するまで待機

    Lambda->>Master: テスト停止を指示<br/>(GET /stop)
    Master->>Worker: 停止指示
    Master-->>Lambda: 停止完了

    Lambda->>Master: テスト結果を取得<br/>(GET /stats/requests)
    Master-->>Lambda: 統計データ(JSON)

    Note over Lambda: サマリを抽出<br/>(全体集計 + POST /orders)

    Lambda->>CWL: サマリをログに出力

    GHA->>CWL: サマリログを検索
    CWL-->>GHA: テスト結果(JSON)

    GHA->>GHA: Slack に結果を通知

こうしてLocustを毎週、自動実行することで、Locustを継続的に使用できる状態を保てるようにしています。

おわりに

LocustはWeb UIからすぐに負荷テストを開始できるのが非常に便利です。

複数のテストシナリオを組み合わせて実行することもできます。

今後、BASEの決済や販売方法のテストシナリオを拡充した上で、テストを並走させることで、本番をよりシミュレーションした負荷テストを実施していければと考えています。

こうした負荷テストの仕組みに興味がありましたら採用情報もぜひご覧ください。

binc.jp