Gatlingからk6へ ── Claude Codeによるシナリオ移行

Gatlingからk6へ ── Claude Codeによるシナリオ移行

はじめに

こんにちは、EC基盤開発本部SRE部の金田、花房、松石です。普段はSREとしてZOZOTOWNのインフラ運用や開発を担当しています。

ZOZOではgatling-operatorをOSSの負荷試験ツールとして公開・運用してきました。しかし、Gatling本体の破壊的変更やメンテナー不足といった課題に直面し、新たな負荷試験ツールとしてk6の導入を進めています。

本記事では、gatling-operatorが抱えていた課題と、k6への移行に至った経緯、そしてClaude Codeを活用した既存シナリオの移行方法についてご紹介します。

目次

背景・課題

gatling-operatorとは

gatling-operatorは、ZOZOがOSSとして公開している負荷試験ツールです。GatlingをKubernetes環境で実行するためのKubernetes Operatorとして実装されており、分散負荷試験の実行やレポートの自動生成などの機能を提供しています。

Gatlingの破壊的変更

gatling-operatorの運用を続ける中で、Gatling本体のバージョンアップに伴う破壊的変更が大きな課題となりました。

Gatling 3.11での変更

Gatling 3.11では、これまで提供されていたgatling.shというGatlingを実行するためのシェルスクリプトが削除されました。代わりに、MavenやGradleといったJavaのビルドツールを使用することが必須となっています。

gatling-operatorはgatling.shを実行することを前提に実装されていたため、この変更に対応するには大幅な修正が必要となりました。また、シナリオやリソースファイルを配置するディレクトリ構成も、ビルドツールを使った実行では基本的に固定されるようになり、後方互換の問題も生じます。

Gatling 3.12での変更

Gatling 3.12では、複数のログファイルをまとめてレポートを作成する機能が削除されました。gatling-operatorは分散実行した各Podのログファイルを集約してレポートを生成する仕組みを採用していたため、この変更は致命的な影響を与えます。

なお、この機能はEnterprise版では引き続き利用可能とのことです。しかし、gatling-operatorはOSS版のGatlingを前提に設計されており、Enterprise版を利用する設計への変更は困難です。

メンテナー不足

gatling-operatorのメンテナーは現状2名体制で運用しています。Gatlingの破壊的変更に追随するための改修工数を考えると、この体制では十分な対応が困難な状況でした。

検討した対応策

これらの課題に対して、以下の3つの対応策を検討しました。

対応策 メリット デメリット
gatling-operatorの改修 既存シナリオ資産がそのまま使える 改修規模が大きく、将来性に懸念
Gatling Enterprise版への移行 メンテナンスコスト削減 金銭的な追加コストが発生
他負荷試験ツールへの移行 メンテナンスコスト削減、OSSなら追加コストなし 既存シナリオ資産の移行が必要

検討の結果、OSSとして広く利用されており、Kubernetes環境での実行もサポートされているk6への移行を選択しました。既存シナリオの移行については、生成AIを活用することで工数を削減できると判断しました。

なぜk6を採用したのか

要件

gatling-operatorのメンテナーが不在なことや育成コストの高さから、新たな負荷試験ツールを模索しました。OSSとしてのコミュニティの大きさやKubernetesで運用する既存基盤に導入できる形が条件です。Gatlingから他の負荷試験ツールに移行するにあたって、以下の3つの要件がありました。

  1. メンテナビリティの高さ: なるべくメンテナンスコストを抑えるために、自社で独自のツールを作成しなくても良いOSSの採用を検討。
  2. 負荷試験結果をレポートとして残せるか: Gatlingを用いた負荷試験では、Gatlingレポートが出力されるため、ZOZOではエビデンスとしてそのレポートを使用していた。そのため、移行先のツールでも結果を見られるというのは重要。
  3. シナリオ作成の容易さ: ZOZOでは、要件によるがバックエンドとSREがシナリオを作成する。そのため、シナリオ作成の際に学習コストやレビューコストがなるべく低い言語を採用する必要がある。

k6を採用した理由

移行先を検討する際、以下のツールが候補に上がりましたが、後述する理由からk6への導入が決まりました。

  • Locust
  • k6

ZOZOでは、事業部単位でSREが存在しており、負荷試験基盤を持っています。その1つとしてWEARチームの負荷試験基盤でk6の導入実績があったため、社内ナレッジを活用できることは大きなメリットでした。

techblog.zozo.com

また、k6はGrafana製のOSSであり、Kubernetes上でk6を動作させるためのk6-operatorがすでにあります。さらにxk6を使用することで、カスタムビルドし簡単に拡張機能などを導入できます。k6-operator、xk6などもGrafana社が作成している公式のOSSであり、メンテナンスを自社で担わなくて良いのは大きなメリットでした。k6-operatorはCRDを導入しており、マニフェストファイルを適用するだけで負荷試験が簡単に実行でき、既存のgatling-operatorと変わらずに運用できる点も魅力です。

さらに、シナリオ作成をする際にJavaScriptで記述できるのもメリットでした。負荷試験シナリオでは、複雑な処理は実装しないため、レビューや学習のコストを抑えられる点は魅力的でした。

以上の理由から、最終的にはk6をPoC的に導入することが決定しました。また実際にk6を検証する中で、足りない機能などは別途、検証・実装・議論をしつつ導入していきました。

導入にあたっての課題と工夫

共通ライブラリの実装

k6への移行を進める中で、Gatlingには存在するものの、k6には標準で用意されていない機能があることに気づきました。特に問題となったのがfeeder機能です。

Gatlingのfeeder機能とは

Gatlingのfeederは、CSVファイルなどの外部データソースからテストデータを読み込み、各仮想ユーザーに順次または循環的にデータを供給する仕組みです。例えば、ログインシナリオで複数のユーザー認証情報を使い回す場合などに重宝します。

// Gatlingでのfeeder使用例
val feeder = csv("users.csv").circular
val scn = scenario("Login")
  .feed(feeder)
  .exec(http("login").post("/auth").body(StringBody("""{"email":"${email}","password":"${password}"}""")))

ZOZOの負荷試験シナリオでは、このfeeder機能が多くのシナリオで使用されていました。ユーザー認証情報や商品ID、セッション情報など、様々なテストデータをfeederで管理していたため、k6への移行にあたって同等の機能が必須でした。

k6標準機能の課題

k6にはCSV読み込み機能はあるものの、Gatlingのfeederのような順次供給・循環利用の機能は標準で提供されていません。

// k6標準のデータ読み込み(feederのような機能はない)
import { SharedArray } from 'k6/data';
const data = new SharedArray('users', function () {
  return JSON.parse(open('./users.json'));
});

この方法では、どのVUがどのデータを使用するかの制御や、データの循環供給を自前で実装する必要がありました。

feederライブラリの実装

そこで、Gatlingのfeeder機能を再現する共通ライブラリを実装しました。このライブラリはk6_scenarios/common_lib/feeder/ディレクトリに配置され、以下の機能を提供します。

// feederライブラリの使用例
import { csv } from '../common_lib/feeder/feeder.js';

// CSVデータを循環的に供給するfeederを作成
const userFeeder = csv(open('./users.csv')).circular();

export default function () {
  // 各VUの実行ごとに次のデータを取得
  const userData = userFeeder.next();

  http.post(`${ENDPOINT}/auth/login`, JSON.stringify({
    email: userData.email,
    password: userData.password
  }));
}

実装したfeederライブラリは、以下の供給戦略をサポートしています。

供給戦略 説明 Gatling相当
circular() データを循環的に供給(末尾に達したら先頭に戻る) csv().circular
random() ランダムにデータを選択 csv().random
queue() 順次供給(データが尽きたらエラー) csv().queue

また、配列データや重み付きデータにも対応しています。

import { csv, array, weighted } from '../common_lib/feeder/feeder.js';

// 配列からfeederを作成
const itemFeeder = array(['item1', 'item2', 'item3']).circular();

// 重み付きでデータを選択
const scenarioFeeder = weighted([
  { weight: 70, data: { type: 'browse' } },
  { weight: 20, data: { type: 'search' } },
  { weight: 10, data: { type: 'purchase' } }
]);

このライブラリを実装したことで、既存のGatlingシナリオで使用していたfeeder機能をk6でも同様に利用できるようになりました。シナリオ移行の際も、feederの使用パターンを変換ルールとして定義することで、スムーズな移行が可能になっています。

シナリオ参照方法の検討

k6-operatorでは、シナリオファイルをRunner Podにマウントする方法として以下の3つが提供されています。

方法 概要 メリット デメリット
ConfigMap シナリオをConfigMapとして登録 シンプルで手軽 容量制限あり(約1MB)、複数ファイル不向き
VolumeClaim PVCにシナリオを配置 ファイルサイズ制限なし、複数ファイル対応 構成が複雑
LocalFile カスタムイメージにシナリオを内包 再現性が高い、外部依存なし 軽微な修正でも再ビルドが必要

gatling-operatorでは、シナリオを含んだコンテナイメージをビルドして実行する方式を採用していました。これはLocalFileに相当する方法です。しかし、シナリオを修正するたびにコンテナイメージのビルドとプッシュが必要となり、開発サイクルが遅くなりがちという課題がありました。

k6への移行にあたり、この開発サイクルを改善したいという狙いもあり、シナリオ参照方法を見直すことにしました。

採用したアプローチ:SharedVolume & LocalFile

検討の結果、公式ドキュメントで紹介されている3つの方法ではなく、VolumeClaimとLocalFileのハイブリッドなアプローチを採用しました。

このアプローチでは、PVC(PersistentVolumeClaim)は使用せず、Podの共有ボリューム(emptyDir)とinitContainerを組み合わせます。initContainerでGitリポジトリからシナリオファイルをクローンし、Runner Containerが共有ボリューム経由でそのファイルを参照する仕組みです。

scenario clone diagram

このアプローチには以下のメリットがあります。

  • PVC不要でシンプル: VolumeClaimのような永続ボリュームの管理が不要で、マニフェスト管理やシステム構成がシンプルになる
  • 自動クリーンアップ: 共有ボリュームはPod削除時にデータも削除されるため、後片付けが不要
  • 開発サイクルの改善: シナリオの修正はコミット&プッシュするだけで反映されるため、コンテナイメージのビルドが不要になる
  • ブランチ指定による柔軟性: 開発中のシナリオも開発ブランチを指定することで、すぐに試験可能

一方で、parallelismを設定してRunner Podが複数になる場合は、Pod分だけGit cloneが実行される点には注意が必要です。ただし、sparse-checkoutを使用して必要なディレクトリのみをクローンすることで、この影響を最小限に抑えています。

Git連携によるシナリオ管理

k6のシナリオファイルは、既存のgatling用シナリオと同様にGitHubリポジトリで管理しています。TestRunリソースのinitContainerでgit-syncを使用し、テスト実行時にリポジトリからシナリオファイルをクローンする仕組みを実装しました。

ディレクトリ構成

zozo-benchmark/
└── k6_scenarios/
    ├── common_lib/           # 共通ライブラリ
    │   ├── feeder/           # テストデータ読み込み用
    │   └── metrics/          # メトリクス処理用
    ├── sample/               # シナリオディレクトリ
    │   ├── testrun.yaml      # TestRunマニフェスト
    │   └── test.js           # シナリオファイル
    └── resources/            # テストデータ
        └── sample/
            └── data.csv

initContainerによるシナリオ取得

TestRunリソースでは、テスト実行前にinitContainerが2段階の処理を行います。

  1. prepare-sparse-checkout: sparse-checkoutのパターンファイルを作成し、必要なディレクトリのみをクローン対象として指定
  2. clone-data: git-syncを使用してGitHubリポジトリから指定ブランチのシナリオファイルをクローン
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
  name: k6-sample
spec:
  parallelism: 2
  arguments: --out experimental-opentelemetry
  runner:
    volumes:
      - name: git-sync-config
        emptyDir: {}
      - name: scenario-data
        emptyDir: {}
    volumeMounts:
      - name: scenario-data
        mountPath: /test
    initContainers:
      - name: prepare-sparse-checkout
        image: busybox:1.36
        command: ["/bin/sh", "-c"]
        env:
          - name: TARGET_SCENARIO
            value: scenario_samples/simple
        args:
          - |
            cat <<EOF > /git-sync-config/sparse-checkout-patterns
            k6_scenarios/common_lib/
            k6_scenarios/resources/
            k6_scenarios/${TARGET_SCENARIO}/
            EOF
        volumeMounts:
          - name: git-sync-config
            mountPath: /git-sync-config
      - name: clone-data
        image: registry.k8s.io/git-sync/git-sync:v4.4.2
        args:
          - --repo=https://<シナリオ格納リポジトリ>.git
          - --link=zozo-benchmark
          - --depth=1
          - --root=/test
          - --one-time
          - --sparse-checkout-file=/git-sync-config/sparse-checkout-patterns
        envFrom:
          - secretRef:
              name: k6-github-app-secrets-xxxx
        env:
          - name: GITSYNC_REF
            value: main  # クローンするブランチを指定
        volumeMounts:
          - name: git-sync-config
            mountPath: /git-sync-config
            readOnly: true
          - name: scenario-data
            mountPath: /test
  script:
    localFile: /test/zozo-benchmark/k6_scenarios/scenario_samples/simple/test.js

この仕組みにより、シナリオファイルの修正をコミット・プッシュするだけで、次回の試験実行時に最新のシナリオが反映されます。GITSYNC_REFでブランチを指定できるため、開発中のシナリオも開発ブランチを指定することですぐに試験できます。

GitHub認証

プライベートリポジトリからのクローンには、GitHub Appsによる認証を使用しています。

認証情報の管理には、AWS Secrets ManagerとExternal Secretsを組み合わせています。GitHub Appsの認証情報をSecrets Managerに保存し、External Secrets経由でKubernetes Secretとして読み込みます。

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: k6-github-app-secrets-xxxx
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: cluster-secretstore
    kind: ClusterSecretStore
  data:
    - secretKey: GITSYNC_GIT_CREDENTIAL
      remoteRef:
        key: k6/github-app
    - secretKey: GITSYNC_GIT_CREDENTIAL_PRIVATEKEY
      remoteRef:
        key: k6/github-app-privatekey

clone-dataコンテナでは、このSecretをenvFromで参照しています。

- name: clone-data
  image: registry.k8s.io/git-sync/git-sync:v4.4.2
  envFrom:
    - secretRef:
        name: k6-github-app-secrets-xxxx

git-syncはこれらの環境変数を自動的に読み取り、GitHub Appsで認証してプライベートリポジトリへのアクセスを実現しています。

Datadogによる試験結果の可視化

Gatlingからk6への移行にあたり、試験結果を可視化・保存する方法についても検討しました。負荷試験基盤を利用するエンジニアの移行ハードルを上げないよう、また、利用体験を損なわずに試験結果をこれまで通りエビデンスとして活用できるよう、以下の要件を満たす必要がありました。

  1. Gatlingレポートと同等のメトリクスや統計情報を可視化できること
  2. 試験結果を一定の期間保存し、後から確認・比較できること

検討の結果、k6のメトリクスをDatadogに送信し、独自のダッシュボードで試験結果を可視化する方式を採用しました。以降では、Datadogダッシュボードを採用した理由と構築方法について説明します。

k6標準のレポート機能の課題

gatling-operatorでは、Gatlingにより出力されたHTMLレポートをAWS S3に保存する機能が存在し、利用者はS3上のファイルにアクセスすることで試験結果を確認できました。k6でも同様の方式で試験結果を保存・確認できないか検討しました。

k6にはHTMLレポートを出力する標準機能であるhandleSummary関数や、よりリッチなHTMLレポートを出力できるbenc-uk/k6-reporterが存在します。しかし、クラウドストレージにレポートを保存する仕組みは存在せず、コストをかけて作り込む必要があったため、この方式は採用しませんでした。

JSON/CSV出力機能を利用する案も検討しましたが、可視化の作り込みにコストがかかり、また上記と同様にレポートを保存する仕組みを構築する必要があったため、こちらも採用しませんでした。

Datadogダッシュボードの採用

他の案も比較検討しましたが、普段から監視ツールとしてDatadogを使用していることから、k6のメトリクスを利用した独自のDatadogダッシュボードを作成する方式を最終的に採用しました。

Datadogなら以下のようにそれぞれの要件を満たせました。

  1. ダッシュボード機能により試験結果を可視化できること
  2. Datadogのメトリクスは長期間保存され1、タグ付けにより試験ごとの絞り込みと比較が可能なこと

既存のDatadog環境を活用できるため、以下のメリットがありました。

  • 低い導入コスト:負荷試験基盤にはdatadog-agentを導入済みのためk6のメトリクス収集が容易
  • 統合的な分析:Datadog上で試験結果と試験中のトレースやカスタムメトリクスを紐付けて確認できる
  • 利用者の認知負荷や学習コストの軽減:使い慣れたダッシュボード機能で柔軟に試験結果を可視化できる

Datadogへのメトリクス送信

k6の試験結果をDatadogダッシュボードで可視化するには、k6のメトリクスをDatadogに送信する必要があります。Datadogへのメトリクス送信には、負荷試験基盤に導入済みのdatadog-agentを活用しました。また、試験結果を区別できるようにメトリクスにタグを付与する工夫も行いました。

OpenTelemetry方式での送信

k6からDatadogにメトリクスを送信する方法として、StatsD方式OpenTelemetry方式が存在します。StatsD方式は拡張機能を含んだコンテナイメージをビルドする必要があったため、Kubernetesマニフェストへの設定追加のみで済むOpenTelemetry方式を採用しました。

以下の手順でk6からDatadogへのメトリクス送信を実現しました。

  1. 公式ドキュメントに従いdatadog-agentのOTLPメトリクス受信を有効化
  2. k6のメトリクス送信設定をConfigMapとして作成
  3. TestRunにConfigMap参照と--out experimental-opentelemetry引数を追加

ConfigMapとTestRun設定例はそれぞれ以下の通りです。

apiVersion: v1
kind: ConfigMap
metadata:
  name: k6-system-config
data:
  K6_OTEL_EXPORTER_TYPE: "grpc"
  K6_OTEL_GRPC_EXPORTER_ENDPOINT: "datadog-agent.kube-support.svc.cluster.local:4317"
  K6_OTEL_SERVICE_NAME: "k6"
  K6_OTEL_METRIC_PREFIX: "k6_"
  K6_OTEL_GRPC_EXPORTER_INSECURE: "true"
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
  name: k6-metrics-sample
spec:
  arguments: --out experimental-opentelemetry
  runner:
    envFrom:
      - configMapRef:
          name: k6-system-config

メトリクスへのタグ付け

Datadogダッシュボードで試験結果を区別するにはメトリクスにタグを付与しておく必要があります。負荷試験基盤で統一的なタグを付与できるように、共通ライブラリとしてperformRequestというHTTPリクエストのラッパー関数を実装しました。

以下が実装したperformRequestです。commonTagsはダッシュボードでの試験結果の絞り込みに利用する共通タグです。また、詳細分析で利用できるリクエスト固有のタグも付与しています。

import { check } from 'k6';
import { commonTags } from './tags.js';

export function performRequest(requestName, requestFunc, expectedStatus = 200) {
  try {
    const estimatedPath = `/${requestName}`;
    const preRequestTags = {
      ...commonTags,
      request_name: requestName,
      path: estimatedPath,
      endpoint: requestName
    };

    const response = requestFunc(preRequestTags);

    const actualPath = getPathFromUrl(response.url) || estimatedPath;
    const statusCategory = getStatusCategory(response.status);

    const checkResult = check(response, {
      [`${requestName}: status is ${expectedStatus}`]: (r) => r.status === expectedStatus,
      [`${requestName}: response time < 5000ms`]: (r) => r.timings.duration < 5000,
    }, {
      ...commonTags,
      request_name: requestName,
      path: actualPath,
      status_code: response.status.toString(),
      status_category: statusCategory,
      endpoint: requestName
    });

    const isSuccess = response.status === expectedStatus;
    if (!isSuccess) {
      console.error(`✗ ${requestName}: ${response.status} - ${actualPath}`);
    }

    return response;

  } catch (error) {
    console.error(`${requestName} error: ${error}`);
    throw error;
  }
}

commonTagsの内容は以下の通りです。

export const commonTags = {
  test_run_id: __ENV.K6_TEST_RUN_ID || console.warn('K6_TEST_RUN_ID not set. Use environment variable for consistent ID across pods.') || `test-run-${new Date().toISOString().slice(0, 16).replace(/[T:]/g, '-')}`,
  env: __ENV.K6_ENV || 'stg',
  scenario: __ENV.K6_SCENARIO || 'default',
  team: __ENV.K6_TEAM || 'default',
};

performRequestを利用するには以下の2つのステップが必要です。

  1. TestRunでの環境変数の設定
  2. シナリオファイル内のリクエスト処理をperformRequestを使用するように書き変える

TestRunの環境変数とシナリオファイルの記述例はそれぞれ以下の通りです。

env:
  - name: K6_TEST_RUN_ID
    value: "member-api-stress-test-20251209"
  - name: K6_ENV
    value: "stg"
  - name: K6_SCENARIO
    value: "member-api-create-and-update-user"
  - name: K6_TEAM
    value: "member-api-team"
import { performRequest } from '../common_lib/metrics/metrics.js';
import http from 'k6/http';

export default function () {
  performRequest('create_user', (tags) => {
      return http.post('https://api.example.com/users', JSON.stringify({
          name: 'Test User',
          email: 'test@example.com'
      }), {
          headers: { 'Content-Type': 'application/json' },
          tags
      });
  }, 200);

  performRequest('update_user', (tags) => {
      return http.put('https://api.example.com/users/123', JSON.stringify({
          name: 'Updated User'
      }), {
          headers: { 'Content-Type': 'application/json' },
          tags
      });
  }, 200)
};

ダッシュボードの構成と拡張性

以下の画像は今回作成したDatadogダッシュボードのメイン部分です。

Datadogダッシュボードのメイン部分

4つのタグにより試験結果を絞り込むことができます。

タグ名 説明
test_run_id テスト実行の識別子
scenario テストシナリオ名
team 実行チーム名
env テスト対象環境

各ウィジェットはGatlingレポートの項目を参考にしつつ、k6固有のメトリクスも確認できる構成にしました。Gatlingレポートと同等以上の試験結果の可視化を実現できたと思います。

さらに、従来のGatlingレポートが固定形式だったのに対し、Datadogダッシュボードでは以下のような柔軟な拡張が可能です。

  • カスタムメトリクスを組み合わせた分析
  • 試験内容や目的に応じたダッシュボードのカスタマイズ
  • チームごとの独自ダッシュボード作成と共有

Claudeサブエージェントを使ったシナリオ移行

k6を導入した後の課題として、Scalaで記述された既存シナリオをどのように移行していくかがありました。Gatlingシナリオが約130個もあるため、全てを手動でk6用に移行するのは困難でした。

さらに、シナリオ移行を実現するにあたって、共通のコンテキストを用いて移行することは必須でした。具体的には以下のようなルールに準拠する必要がありました。

  • feeder機能やDatadogにタグ付きメトリクスを送信するための共通ライブラリを使用すること
  • シナリオをGit連携で使えるようにする一方、秘匿情報(ログイン・パスワードなど)が含まれたCSVファイルはAWS Secrets Managerから取得すること

これらの要件から、Claudeサブエージェントを用いたシナリオ移行を検証しました。

Claudeサブエージェントとは

Claude Codeの機能で、より専門的なタスクに用いることができ、コンテキスト管理を容易にすることが可能となります。より詳細な内容は、公式ドキュメントをご覧ください。

code.claude.com

シナリオ移行での活用方法

シナリオ移行にあたって、以下のような使用方法を検証しました。まずclaudeコマンドでClaude Codeを起動します。

$ claude

Claude Codeの起動後、サブエージェントを使用してGatlingシナリオをk6に移行したい旨の指示を送信します。その後、サブエージェントからの質問に対して回答します。以下の具体例では、5. Type something.に対象のシナリオパス(simulations/ZozoSessionApiBasicLoadTest.scala)を入力しています。

> sub-agentを使用してGatling(Scala)シナリオをk6に移行したい

⏺ 了解しました。Gatling(Scala)シナリオをk6に移行するためのサブエージェントを起動します。

  まず、移行対象のGatlingシナリオを特定するため、現在のリポジトリ構造を確認させてください。

⏺ 多数のGatlingシナリオがあります。どのシナリオを移行したいか確認させてください。

 ☐ 移行対象

どのGatlingシナリオをk6に移行しますか?特定のシナリオを指定するか、移行対象のカテゴリを教えてください。

❯ 1. member-id-pf-sre関連
     zozo-id, zozo-session-api, zozo-member-api等のシナリオ
  2. 特定のファイルを指定
     ファイル名またはパスで個別に指定
  3. すべてのシナリオ
     simulations/配下の全シナリオを順次移行
  4. その他
     カスタムで指定
  5. Type something.

Enter to select · Tab/Arrow keys to navigate · Esc to cancel

サブエージェントが起動し、移行処理が実行されます。

⏺ simulations/ZozoSessionApiBasicLoadTest.scalaをk6に移行します。k6-scenario-migratorエージェントを起動します。

⏺ k6-scenario-migrator(Migrate ZozoSessionApi to k6)
  ⎿

     +13 more tool uses (ctrl+o to expand)

· Sketching… (esc to interrupt · 2m 38s · ↓ 440 tokens)

以下が移行結果です。

⏺ 移行完了 🎉

  simulations/ZozoSessionApiBasicLoadTest.scala を k6 に正常に移行しました。

  作成されたファイル

  | ファイル       | パス                                                                                |
  |------------|-----------------------------------------------------------------------------------|
  | k6シナリオ     | k6_scenarios/scenario_member_id_sre/zozo_session_api/basic_load_test/test.js      |
  | TestRun CR | k6_scenarios/scenario_member_id_sre/zozo_session_api/basic_load_test/testrun.yaml |

  シナリオ概要

  Webシナリオ (ZOZO_SESSION_PLATFORM=web):
  1. Init Session × 3回
  2. Save Session × 3回
  3. Get Session × 57回

  Appシナリオ (ZOZO_SESSION_PLATFORM=app):
  1. Init Session × 1回
  2. Get Session × 100回

  Gatling → k6 変換ポイント

  | Gatling               | k6                             |
  |-----------------------|--------------------------------|
  | constantUsersPerSec   | constant-arrival-rate executor |
  | check(status.is(200)) | performRequest() + thresholds  |
  | jsonPath().saveAs()   | JSON.parse() + 変数保存            |

  実行方法

  kubectl apply -f k6_scenarios/scenario_member_id_sre/zozo_session_api/basic_load_test/testrun.yaml

どのように実現しているか

以下のようなディレクトリ構造で、サブエージェントを作成しています。

.claude
├── agents
│   └── k6-scenario-migrator.md  # サブエージェントに対する指示やチェックリストを記載
├── docs
│   └── scala-to-k6-migration-guide.md  # 変換ガイドライン(具体的なライブラリの使い方や変換ルールを記載)
└── settings.local.json

前提として、それぞれのプロンプトはコンフルエンス上で整備した社内向けのk6ユーザーガイドラインをもとにしています。このユーザーガイドラインをコンテキストとして、サブエージェント用のプロンプトを作成しました。サブエージェント(.claude/agents/k6-scenario-migrator.md)には「何をすべきか」を指示しています。変換ガイドライン(.claude/docs/scala-to-k6-migration-guide.md)には「どう変換すべきか」の詳細ルールを記載しています。

シナリオ移行の品質を担保するため、サブエージェントでは以下の4つの観点でプロンプトを設計しました。

まず、事前質問による情報収集です。エージェント起動時にチーム名・マイクロサービス名・対象ファイルを確認させることで、後続の処理に必要な情報を漏れなく取得します。

#### 質問1: チーム名の確認
このシナリオを管轄するチーム名を教えてください。
例: member-id-pf-sre, cart-sre, search-sre

#### 質問2: マイクロサービス名の確認
対象となるマイクロサービス名を教えてください。
例: zozo-id, zozo-session-api, zozo-member-api
注意: 「-api」サフィックスは除去し、「-」は「_」に変換します。

#### 質問3: Scalaシナリオファイルパスの確認
移行元のScalaシナリオファイルのパスを教えてください。
例: simulations/ZozoIdLogin.scala

次に、命名規則の明確化です。ディレクトリ名やシナリオ名の移行ルールを明示することで、一貫性のあるディレクトリ、ファイルの作成を実現しています。

#### マイクロサービス名からディレクトリ名を生成
1. 「-api」サフィックスを除去
   - zozo-session-api → zozo-session
2. ハイフン「-」をアンダースコア「_」に変換
   - zozo-session → zozo_session

#### Scalaファイル名からシナリオ名を生成
1. キャメルケースをスネークケースに変換(ディレクトリ名用)
   - ZozoIdLogin → zozo_id_login
2. 環境変数: K6_SCENARIOはハイフン繋ぎに統一
   - zozo_id_login → zozo-id-login
   - 理由: Datadogメトリクスで一貫性を保つため

#### 最終的なディレクトリパス
k6_scenarios/scenario_<チーム名>/<マイクロサービス名>/<scenario_name>/

続いて、チェックリストによる品質担保です。移行完了後に確認すべき項目をリスト化し、共通ライブラリの使用やDatadogタグの設定漏れを防いでいます。

- [ ] `performRequest`関数を使用している
- [ ] `tags`パラメータをすべてのHTTPリクエストに渡している
- [ ] feederライブラリ(`csv`, `array`など)を適切に使用している
- [ ] 環境変数にデフォルト値を設定している
- [ ] K6_TEST_RUN_ID, K6_ENV, K6_SCENARIO, K6_TEAMを設定している
- [ ] testrun.yamlの`K6_SCENARIO`とtest.jsの`options.scenarios`のキー名が同じ(ハイフン繋ぎ)になっている
- [ ] testrun.yamlに`cleanup: post`を設定している(テスト終了後にPodを自動削除するため)
- [ ] CSVデータに秘匿情報が含まれるかどうかを判断している
- [ ] 秘匿情報を含むCSVの場合、External Secretsで管理している
- [ ] 秘匿情報を含むCSVの場合、ExternalSecretマニフェスト(secrets.yaml)を作成している
- [ ] 秘匿情報を含むCSVの場合、AWS Secrets ManagerにCSV形式のプレーンテキストデータを登録する必要があることをユーザーに伝えている
- [ ] 秘匿情報を含まないCSVの場合、`k6_scenarios/resources/scenario_<チーム名>/`配下に配置している
- [ ] CSVデータをExternal Secretsから読み込む場合、適切にマウント・読み込み処理を実装している
- [ ] CSVデータをローカルファイルから読み込む場合、正しい相対パスを使用している
- [ ] CSVデータ読み込み時のエラーハンドリングが実装されている
- [ ] ディレクトリ構成が `scenario_<チーム名>/<マイクロサービス名>/<scenario_name>` に準拠している
- [ ] 相対パスでのインポートが正しい(`../../../common_lib/...`- [ ] testrun.yamlが作成されている(**必須**)
- [ ] testrun.yamlのlocalFileパスが正しい
- [ ] testrun.yamlのTARGET_SCENARIOパスが正しい
- [ ] testrun.yamlのmetadata.nameが適切に設定されている
- [ ] 存在しない・不要なimportは行わない
  - [ ] ex `import { open } from 'k6';`

次に、セキュリティを考慮した分岐です。CSVデータの性質(秘匿情報の有無)に応じて処理を分岐させ、機密情報をリポジトリへ含めないようにしています。

#### パターン1: 秘匿情報を含むCSV(External Secrets使用)
対象: ログイン情報、APIトークンなどの秘匿情報
→ AWS Secrets Managerに登録し、External Secretsでマウント

#### パターン2: 秘匿情報を含まないCSV(ローカルファイル)
対象: 商品一覧、カテゴリ情報などの公開データ
→ k6_scenarios/resources/scenario_<チーム名>/ に配置

最後に、入力例と出力例の記載です。SREに加え開発者がこのシナリオ移行を実施する可能性があるため、移行後の動作方法などを具体的に明記し、すぐに動作確認を実施できる出力を目指しました。

##### 入力例

```
チーム名: member-id-pf-sre
マイクロサービス名: zozo-session-api
Scalaファイル: simulations/ZozoSessionProxyTest.scala
```

### 出力例

```
✓ k6シナリオの生成が完了しました

生成されたファイル:
- k6_scenarios/scenario_member_id_sre/zozo_session/zozo_session_proxy_test/test.js
- k6_scenarios/scenario_member_id_sre/zozo_session/zozo_session_proxy_test/testrun.yaml
- k6_scenarios/scenario_member_id_sre/zozo_session/zozo_session_proxy_test/README.md

ディレクトリ構造:
k6_scenarios/scenario_member_id_sre/zozo_session/zozo_session_proxy_test/
├── test.js
├── testrun.yaml
└── README.md

実行方法(Kubernetes):
# TestRunの適用
kubectl apply -f k6_scenarios/scenario_member_id_sre/zozo_session/zozo_session_proxy_test/testrun.yaml

# ログ確認
kubectl logs -l k6_cr=zozo-session-proxy-test --tail=100 -f

# TestRunの削除
kubectl delete testrun zozo-session-proxy-test

注意: ローカル実行(k6コマンド)は使用しません。必ずKubernetes上で実行してください。
```

以上が、サブエージェントに記載している主なプロンプトの内容です。

サブエージェントで使用する変換ガイドラインは、社内用のk6使用方法ドキュメントを落とし込む形で作成しています。内容としては、具体的なライブラリの使用例やシナリオを移行する際の具体的なBefore/Afterのソースコードを記載しています。

下記に、ガイドラインとなるプロンプトの全文を記載します。

プロンプトの全文

# Gatling (Scala) から k6 (JavaScript) へのシナリオ移行プロンプト

## 概要

このドキュメントは、既存のGatling(Scala)で記述された負荷試験シナリオを、k6(JavaScript)に移行するための生成AIプロンプトです。
zozo-benchmarkリポジトリの共通ライブラリを活用し、効率的かつ標準化されたk6シナリオを生成します。

## 前提条件

- zozo-benchmarkリポジトリの既存実装を参照すること
- k6-operatorのTestRun CRDを使用したKubernetes環境での実行を想定
- 共通ライブラリ(feeder, metrics)を活用すること
- External Secretsでシークレット管理を行うこと

---

## 移行プロンプト

以下のプロンプトを生成AIに入力してください。Scalaシナリオを添付することで、k6シナリオへ変換されます。

```
# タスク

以下のGatling(Scala)シナリオをk6(JavaScript)シナリオに変換してください。
zozo-benchmarkリポジトリの標準に従い、共通ライブラリとベストプラクティスを適用してください。

---

# 変換ルール

## 1. 基本構造

### 環境変数の取得
**Scala (Before)**
```scala
val endpoint = sys.env.getOrElse("ENDPOINT", "xxxx")
val clientToken = sys.env.getOrElse("CLIENT_TOKEN", "dummy123456789")
val usersPerSec = sys.env.getOrElse("CONCURRENCY", "1").toInt
val durationSec = sys.env.getOrElse("DURATION", "10").toInt
```

**JavaScript (After)**
```javascript
// オプションで使用する環境変数には必ずデフォルト値を設定
const ENDPOINT = __ENV.ENDPOINT || 'xxxx';
const CLIENT_TOKEN = __ENV.CLIENT_TOKEN || 'dummy123456789';
const VUS = __ENV.VUS ? parseInt(__ENV.VUS) : 1;
const RATE = __ENV.RATE ? parseInt(__ENV.RATE) : 1;
const DURATION = __ENV.DURATION || '10s';
```

## 2. HTTPプロトコル設定とヘッダー

### HTTPプロトコルとベースURL
**Scala (Before)**
```scala
val httpProtocol = http
  .baseUrl(endpoint)
  .userAgentHeader("Mozilla/5.0...")
  .shareConnections

val requestHeaders = Map(
  "Content-Type" -> "application/json;charset=UTF-8",
  "Accept-Encoding" -> "gzip, deflate",
  "accept" -> "application/json",
  "zozo-api-client-token" -> clientToken
)
```

**JavaScript (After)**
```javascript
// k6ではベースURLをオプションとして設定せず、リクエストごとにフルURLを指定
// 共通ヘッダーは変数として定義し、リクエスト時に使用
const commonHeaders = {
  'Content-Type': 'application/json;charset=UTF-8',
  'Accept-Encoding': 'gzip, deflate',
  'Accept': 'application/json',
  'zozo-api-client-token': CLIENT_TOKEN,
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0'
};
```

## 3. フィーダー(テストデータ)

### CSVデータの読み込みパターン

CSVデータの読み込み方法は、**データの性質によって2つのパターン**に分かれます:

#### パターン1: 秘匿情報を含むCSV(External Secrets使用)

**対象データ:**
- ログイン情報(ユーザー名、パスワード)
- APIトークン、アクセスキー
- その他の認証情報

**重要:** 秘匿情報を含むCSVファイルは、**リポジトリに含めず**、External Secretsを使用してAWS Secrets Managerから読み込みます。

**手順:**

1. **AWS Secrets ManagerにCSVデータを登録**
   ```
   # AWS Secrets ManagerにプレーンテキストとしてCSV内容を保存
   # キー: zozo-id/login_info
   # 値: CSV形式のプレーンテキスト
   user_id,password
   test_user_1,password123
   test_user_2,password456
   ```

2. **ExternalSecretマニフェストを作成** (`secrets.yaml`)
   ```yaml
   apiVersion: external-secrets.io/v1beta1
   kind: ExternalSecret
   metadata:
     name: zozo-id-login-info-secrets-xxxx
     namespace: default
   spec:
     refreshInterval: "0"
     secretStoreRef:
       name: cluster-secretstore
       kind: ClusterSecretStore
     data:
       - secretKey: login_info_zozo_id
         remoteRef:
           key: zozo-id/login_info
   ```

3. **testrun.yamlでSecretをマウント**
   ```yaml
   spec:
     runner:
       volumes:
         - name: login-secrets
           secret:
             secretName: zozo-id-login-info-secrets-xxxx
       volumeMounts:
         - name: login-secrets
           mountPath: /secrets
           readOnly: true
   ```

4. **test.jsでSecretから読み込み**
   ```javascript
   import { csv } from '../../../common_lib/feeder/feeder.js';
   import { open } from 'k6';

   // Load login credentials from Secret (mounted at /secrets/login_info_zozo_id)
   let loginFeeder;
   try {
     const secretData = open('/secrets/login_info_zozo_id');
     if (!secretData || secretData.trim() === '') {
       throw new Error('Secret file is empty');
     }
     loginFeeder = csv(secretData, {}, 'login-info');
     console.log('[INFO] Loaded login info from Secret: /secrets/login_info_zozo_id');
   } catch (error) {
     console.error('[ERROR] Failed to load login info from Secret:', error.message);
     console.error('[DEBUG] Check: kubectl get secret <secret-name> -o yaml');
     throw new Error('Login info data source not available. Secret is required.');
   }

   // default関数内で使用
   export default function () {
     const userData = loginFeeder.circular();
     // userData.user_id, userData.password などが利用可能
   }
   ```

**参照実装:**
- ExternalSecretマニフェスト: `k6_scenarios/scenario_member_id_sre/zozo_id/secrets.yaml`
- test.js実装: `k6_scenarios/scenario_member_id_sre/zozo_id/login_scenario/test.js` (44-55行目)

---

#### パターン2: 秘匿情報を含まないCSV(ローカルファイル)

**対象データ:**
- 商品一覧
- カテゴリ情報
- テスト用の公開データ

**配置場所:**
```
k6_scenarios/resources/scenario_<team_name>/
└── *.csv  # 例: products.csv, categories.csv など

# チーム別の例
k6_scenarios/resources/scenario_member_id_sre/
├── products.csv
└── categories.csv

k6_scenarios/resources/scenario_cart_sre/
├── cart_items.csv
└── user_sessions.csv
```

**注意:**
- 基本は `scenario_<team_name>/` 直下に配置
- 整理のため、さらにサブディレクトリを作ることも可能(例: `scenario_samples/feeder/`- サブディレクトリを作る場合は、importパスを適切に調整してください

**Scala (Before)**
```scala
val feeder = csv("products.csv")

// シナリオ内で使用
.feed(feeder.circular)
```

**JavaScript (After)**
```javascript
import { csv } from '../../../common_lib/feeder/feeder.js';
import { open } from 'k6';

// CSVファイルを読み込み(resourcesディレクトリから相対パスで参照)
// パスの例: scenario_member_id_sre のチームの場合
const csvData = open('../../../resources/scenario_member_id_sre/products.csv');
const productsFeeder = csv(csvData, {}, 'products');

// default関数内で使用
export default function () {
  // circular(): 順番に1つずつ選択(ラウンドロビン)
  const product = productsFeeder.circular();

  // random(): ランダムに選択
  // const product = productsFeeder.random();

  // perVu(): VUごとに固定データを割り当て
  // const product = productsFeeder.perVu();

  // productには CSV の各カラムがプロパティとして含まれる
  // 例: product.product_id, product.name, product.price など
}
```

### Feederのメソッド
- `circular()`: ラウンドロビンで順番に選択(Gatlingのcircularと同じ)
- `random()`: ランダムに選択(Gatlingのrandomと同じ)
- `perVu()`: VUごとに固定データを割り当て(Gatlingのqueueと同じ)

### AtomicIntegerの変換(ユニークID生成)

**重要**: k6にはGatlingの`AtomicInteger`のようなスレッドセーフなカウンターがありません。
複数のVUが同時に実行される環境でユニークなIDを生成するには、VU IDとイテレーション番号を組み合わせる必要があります。

**Scala (Before)**
```scala
// 初期値は環境変数のCURRENT_MAX_MEMBER_ID
val memberIdCounter = new AtomicInteger(currentMaxMemberId)
// カスタムfeederを作成し、member_idをインクリメントしていく
val memberIdFeeder = Iterator.continually(Map("member_id" -> memberIdCounter.incrementAndGet()))

var single_request = scenario("test scenario")
  .feed(memberIdFeeder)
  .exec(...)
```

**JavaScript (After)**
```javascript
import exec from 'k6/execution';

const CURRENT_MAX_MEMBER_ID = __ENV.CURRENT_MAX_MEMBER_ID ? parseInt(__ENV.CURRENT_MAX_MEMBER_ID) : 0;

export default function () {
  // 各VUに独立したID空間を割り当ててユニークなIDを生成
  // VU 1: 1000001, 1000002, 1000003, ...
  // VU 2: 2000001, 2000002, 2000003, ...
  // これにより、複数のVUが同時に実行しても重複が発生しない
  const memberId = CURRENT_MAX_MEMBER_ID + (exec.vu.idInTest * 1000000) + exec.scenario.iterationInInstance + 1;

  // memberIdを使用したリクエスト
  performRequest('example', (tags) => {
    return http.post(`${ENDPOINT}/api/endpoint`,
      JSON.stringify({ member_id: memberId }),
      { headers: { ...commonHeaders }, tags }
    );
  }, 200);
}
```

**説明**:
- `exec.vu.idInTest`: VUごとに一意のID(1, 2, 3, ...)
- `exec.scenario.iterationInInstance`: そのVU内でのイテレーション番号(0, 1, 2, ...)
- `1000000`: 各VUに100万のIDスペースを確保(VUごとに100万回以上イテレーションする場合は調整が必要)
- この方法により、409 ConflictなどのID重複エラーを防止できます

**生成例**:
- `CURRENT_MAX_MEMBER_ID=0`の場合:
  - VU 1, iteration 0: 1000001
  - VU 1, iteration 1: 1000002
  - VU 2, iteration 0: 2000001
  - VU 2, iteration 1: 2000002

## 4. HTTPリクエスト

### リクエストの変換(performRequest関数を使用)

**重要**: 必ず`performRequest`関数を使用してください。これにより:
- Datadogへのメトリクス送信
- カスタムタグの自動付与
- 標準化されたエラーハンドリング

が実現されます。

**Scala (Before)**
```scala
val login = exec(
  http("Login")
    .post("/auth/v1/login")
    .headers(requestHeaders)
    .body(StringBody("""{"id": "${email}","password": "${password}"}"""))
    .check(status.is(200))
    .check(jsonPath("$.id_token").saveAs("id_token"))
    .check(jsonPath("$.refresh_token").saveAs("refresh_token"))
)
```

**JavaScript (After)**
```javascript
import { performRequest } from '../common_lib/metrics/metrics.js';
import http from 'k6/http';

export default function () {
  const userData = userFeeder.circular();

  // performRequest(リクエスト名, リクエスト関数, 期待するステータスコード)
  const loginResponse = performRequest('login', (tags) => {
    const payload = JSON.stringify({
      id: userData.email,
      password: userData.password
    });

    return http.post(`${ENDPOINT}/auth/v1/login`, payload, {
      headers: {
        ...commonHeaders,
      },
      tags  // 必ずtagsを渡す(Datadogのカスタムタグ付与のため)
    });
  }, 200);

  // レスポンスからデータを取り出す
  const loginData = JSON.parse(loginResponse.body);
  const idToken = loginData.id_token;
  const refreshToken = loginData.refresh_token;
}
```

### 各種HTTPメソッドの例

**GET リクエスト**
```javascript
performRequest('healthcheck', (tags) => {
  return http.get(`${ENDPOINT}/auth/v1/healthcheck/db`, {
    headers: commonHeaders,
    tags
  });
}, 200);
```

**POST リクエスト(JSONボディ)**
```javascript
performRequest('create_user', (tags) => {
  const payload = JSON.stringify({
    email: userData.email,
    zozo_id: userData.zozo_id,
    password: userData.password_hash
  });

  return http.post(`${ENDPOINT}/auth/v1/members/${userData.user_id}`, payload, {
    headers: commonHeaders,
    tags
  });
}, 200);
```

**PUT リクエスト**
```javascript
performRequest('update_user', (tags) => {
  const payload = JSON.stringify({
    email: userData.email,
    zozo_id: userData.zozo_id,
    password: userData.password_hash
  });

  return http.put(`${ENDPOINT}/auth/v1/members/${userData.user_id}`, payload, {
    headers: commonHeaders,
    tags
  });
}, 200);
```

**DELETE リクエスト**
```javascript
performRequest('delete_user', (tags) => {
  return http.del(`${ENDPOINT}/auth/v1/members/${userData.user_id}`, null, {
    headers: commonHeaders,
    tags
  });
}, 200);
```

**認証ヘッダー付きリクエスト**
```javascript
performRequest('logout', (tags) => {
  return http.post(`${ENDPOINT}/auth/v1/logout`, null, {
    headers: {
      ...commonHeaders,
      'Authorization': `Bearer ${idToken}`
    },
    tags
  });
}, 200);
```

## 5. シナリオとオプション設定

### シナリオ設定の変換

**重要**: testrun.yamlの`K6_SCENARIO`環境変数とtest.jsの`options.scenarios`のキー名は**必ず同じ名前(ハイフン繋ぎ)**にしてください。

理由:
- `http_req`メトリクス: performRequest関数で`K6_SCENARIO`の値がタグとして付与される
- `iterations`メトリクス: k6のエグゼキュータが`options.scenarios`のキー名をタグとして自動付与する
- 名前が異なると、Datadogダッシュボードで2つの異なるscenario名が表示され、メトリクスが分断される

**Scala (Before)**
```scala
val single_request = scenario("zozo-id login")
  .feed(feeder.circular)
  .exec(captcha)
  .exec(login)
  .exec(refresh_token)
  .exec(validate_token)
  .exec(logout)

setUp(
  single_request.inject(
    constantUsersPerSec(usersPerSec) during(durationSec seconds)
  ).protocols(httpProtocol)
)
```

**JavaScript (After)**
```javascript
import { sleep } from 'k6';

// オプション設定
// 注意: scenariosのキー名は testrun.yamlのK6_SCENARIOと同じハイフン繋ぎにする
export const options = {
  scenarios: {
    'zozo-id-login': {  // testrun.yamlのK6_SCENARIO="zozo-id-login"と一致させる
      // 一定のレートでリクエストを送信
      executor: 'constant-arrival-rate',

      // 1秒あたりのイテレーション数
      rate: RATE,

      // レートの適用期間
      timeUnit: '1s',

      // 事前に確保するVU数
      preAllocatedVUs: VUS,

      // テスト期間
      duration: DURATION,

      // 最大VU数(必ずpreAllocatedVUs以上)
      maxVUs: 100,
    },
  },
};

// セットアップ処理(テスト開始時に1回だけ実行)
export function setup() {
  console.log('Execution parameters:', JSON.stringify({
    ENDPOINT,
    VUS,
    RATE,
    DURATION,
  }, null, 2));
}

// メインのテスト処理
export default function () {
  const userData = userFeeder.circular();

  // リクエストを順番に実行
  performRequest('captcha', (tags) => {
    return http.get(`${ENDPOINT}/auth/v1/irregular-login/detect`, {
      headers: commonHeaders,
      tags
    });
  }, 200);

  const loginResponse = performRequest('login', (tags) => {
    const payload = JSON.stringify({
      id: userData.email,
      password: userData.password
    });
    return http.post(`${ENDPOINT}/auth/v1/login`, payload, {
      headers: commonHeaders,
      tags
    });
  }, 200);

  const loginData = JSON.parse(loginResponse.body);
  const idToken = loginData.id_token;
  const refreshToken = loginData.refresh_token;

  // 以降のリクエストを続ける...

  // 必要に応じてsleepを追加(Think Time)
  // sleep(1);
}

// クリーンアップ処理(テスト終了時に1回だけ実行)
export function teardown(data) {
  console.log('Test completed');
}
```

### Executorの選択

Gatlingの`constantUsersPerSec`は、k6の`constant-arrival-rate`に対応します。

**その他のExecutorオプション**

1. **constant-vus**: 固定数のVUを一定期間実行
```javascript
executor: 'constant-vus',
vus: 10,
duration: '30s',
```

2. **ramping-vus**: VU数を段階的に増減
```javascript
executor: 'ramping-vus',
startVUs: 0,
stages: [
  { duration: '30s', target: 10 },
  { duration: '1m', target: 10 },
  { duration: '30s', target: 0 },
],
```

3. **ramping-arrival-rate**: 到着レートを段階的に増減
```javascript
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 10,
maxVUs: 100,
stages: [
  { duration: '30s', target: 50 },
  { duration: '1m', target: 50 },
  { duration: '30s', target: 0 },
],
```

## 6. Datadogメトリクスとカスタムタグ

### 環境変数でカスタムタグを設定

TestRun YAMLファイル(`testrun.yaml`)で以下の環境変数を設定してください:

```yaml
spec:
  runner:
    env:
      # テスト実行ID(必須:Datadogダッシュボードでのフィルタリングに使用)
      - name: K6_TEST_RUN_ID
        value: "zozo-id-login-stg-2025-10-22"

      # 環境名(必須)
      - name: K6_ENV
        value: "stg"

      # シナリオ名(必須:test.jsのoptions.scenariosのキー名と一致させる)
      - name: K6_SCENARIO
        value: "zozo-id-login"

      # チーム名(必須)
      - name: K6_TEAM
        value: "member-id-pf-sre"

      # その他の環境変数
      - name: ENDPOINT
        value: "xxxx"
      - name: VUS
        value: "10"
      - name: RATE
        value: "5"
      - name: DURATION
        value: "60s"
```

### カスタムメトリクスの生成(オプション)

基本的なメトリクスは自動的にDatadogに送信されますが、カスタムメトリクスを追加する場合は以下のようにします:

**注意**: カスタムメトリクスを生成する場合コストが発生するため、@platform-sreチームに相談してください。

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

// カスタムメトリクスの定義
const errorCounter = new Counter('custom_errors');
const loginDuration = new Trend('custom_login_duration');
const successRate = new Rate('custom_success_rate');

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

  const loginResponse = performRequest('login', (tags) => {
    // ... リクエスト処理
  }, 200);

  if (loginResponse.status === 200) {
    successRate.add(1);
    loginDuration.add(Date.now() - startTime);
  } else {
    successRate.add(0);
    errorCounter.add(1);
  }
}
```

## 7. External Secretsの使用

### シークレットの参照

TestRun YAMLファイルでExternal Secretsを参照します:

```yaml
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
  name: zozo-id-login-test
spec:
  parallelism: 4
  script:
    localFile: /test/zozo-benchmark/k6_scenarios/zozo-id-login/test.js
  runner:
    image: grafana/k6:0.54.0
    env:
      # External Secretsからシークレットを取得
      - name: CLIENT_TOKEN
        valueFrom:
          secretKeyRef:
            name: zozo-id-client-token-secret
            key: token

      # ConfigMapから設定を取得
      - name: K6_TEST_RUN_ID
        value: "zozo-id-login-stg-2025-10-22"

    envFrom:
      # GitHub認証用シークレット(必須)
      - secretRef:
          name: k6-github-app-secrets-xxxx

      # Datadog設定用ConfigMap(必須)
      - configMapRef:
          name: k6-system-config
```

### Secretsの管理場所

シークレット情報は以下で管理されます:
- **k6-github-app-secrets**: GitHub認証用(シナリオクローン用)
  - `k8s/main/k6-secrets/secrets.yaml`
- **gatling-secrets**: 既存のGatlingシークレット(API tokenなど)
  - `k8s/main/gatling-secrets/secrets.yaml`

## 8. ディレクトリ構成

### シナリオファイルの配置

```
zozo-benchmark/
└── k6_scenarios/
    ├── common_lib/                                # 共通ライブラリ
    │   ├── feeder/
    │   │   └── feeder.js
    │   └── metrics/
    │       ├── metrics.js
    │       └── tags.js
    ├── resources/                                 # テストデータ(チーム別に管理)
    │   └── scenario_<チーム名>/                   # チーム別リソースディレクトリ
    │       └── *.csv                              # 秘匿情報を含まないCSVファイル
    └── scenario_<チーム名>/                      # チーム別シナリオディレクトリ
        └── <マイクロサービス名>/                 # マイクロサービス別ディレクトリ
            └── <scenario_name>/                   # シナリオ名ディレクトリ
                ├── testrun.yaml                   # TestRun マニフェスト
                ├── test.js                        # シナリオファイル
                └── README.md                      # ドキュメント(オプション)
```

**例**: member-id-pf-sreチームのzozo-oidc-provider-apiのAuthorizationCodeFlowテストの場合
```
zozo-benchmark/
└── k6_scenarios/
    ├── resources/
    │   └── scenario_member_id_sre/
    │       └── test_data.csv
    └── scenario_member_id_sre/
        └── zozo_oidc_provider/
            └── zozo_oidc_provider_api_authorization_code_flow_test/
                ├── testrun.yaml
                ├── test.js
                └── README.md
```

**命名規則**:
- **ディレクトリ名**: Scalaファイル名をスネークケース(アンダーバー繋ぎ)に変換
  - 例: `ZozoOidcProviderApiAuthorizationCodeFlowTest.scala``zozo_oidc_provider_api_authorization_code_flow_test/`
- **K6_SCENARIOとoptions.scenariosのキー名**: ハイフン繋ぎに統一
  - 例: `zozo-oidc-provider-api-authorization-code-flow-test`
  - 理由: Datadogメトリクスで一貫性を保つため

### 相対パスでのインポート

```javascript
// feederライブラリのインポート
import { csv, array } from '../../../common_lib/feeder/feeder.js';

// metricsライブラリのインポート
import { performRequest } from '../../../common_lib/metrics/metrics.js';

// CSVファイルの読み込み(resourcesディレクトリから)
// パスの例: scenario_member_id_sre のチームの場合
const csvData = open('../../../resources/scenario_member_id_sre/data.csv', 'r');
```

**例**: member-id-pf-sreチームのzozo-oidc-providerのAuthorizationCodeFlowテストの場合
```javascript
import { csv, array } from '../../../common_lib/feeder/feeder.js';
import { performRequest } from '../../../common_lib/metrics/metrics.js';
const csvData = open('../../../resources/scenario_member_id_sre/data.csv', 'r');
```

---

# 変換対象のScalaシナリオ

以下のScalaシナリオをk6に変換してください:

[ここにScalaシナリオのコードを貼り付け]

---

# 期待する出力

以下の2つのファイルを生成してください:

1. **test.js**: k6シナリオファイル
   - 上記のルールに従って変換
   - すべてのインポートを含める
   - コメントで各セクションを説明

2. **testrun.yaml**: TestRun マニフェストファイル
   - 適切なリソース設定
   - 環境変数とシークレットの設定
   - initContainersの設定(シナリオクローン用)

3. **README.md** (オプション): シナリオの説明
   - テスト目的
   - 実行方法
   - 必要な環境変数
```

---

## 使用例

### 1. Scalaシナリオを準備

```scala
// simulations/ZozoIdLogin.scala
class ZozoIdLogin extends Simulation {
  val endpoint = sys.env.getOrElse("ENDPOINT", "xxxx")
  // ... (既存のScalaコード)
}
```

### 2. 生成AIに入力

上記の「移行プロンプト」全体をコピーし、最後の「変換対象のScalaシナリオ」セクションにScalaコードを貼り付けて、生成AIに入力します。

### 3. 生成されたk6シナリオを確認

生成AIが以下を出力します:
- `test.js`: k6シナリオファイル
- `testrun.yaml`: TestRun マニフェスト
- `README.md`: 説明ドキュメント(オプション)

### 4. リポジトリに配置

```bash
# シナリオディレクトリを作成
# 例: member-id-pf-sreチームのzozo-idのLoginテストの場合
mkdir -p k6_scenarios/scenario_member_id_sre/zozo_id/zozo_id_login

# ファイルを配置
mv test.js k6_scenarios/scenario_member_id_sre/zozo_id/zozo_id_login/
mv testrun.yaml k6_scenarios/scenario_member_id_sre/zozo_id/zozo_id_login/
mv README.md k6_scenarios/scenario_member_id_sre/zozo_id/zozo_id_login/

# CSVファイルがある場合(秘匿情報を含まないもののみ)
mv *.csv k6_scenarios/resources/
```

### 5. テスト実行

```bash
# TestRunを適用
# 例: member-id-pf-sreチームのzozo-idのLoginテストの場合
kubectl apply -f k6_scenarios/scenario_member_id_sre/zozo_id/zozo_id_login/testrun.yaml

# ログ確認
kubectl get testruns
kubectl logs <pod-name>

# Datadogダッシュボードで結果確認

[Datadogダッシュボードのリンク]

```

---
## 参考資料
- [k6 公式ドキュメント](https://grafana.com/docs/k6/latest/)
- [k6-operator GitHub](https://github.com/grafana/k6-operator)

---

## トラブルシューティング

### タグが付与されない場合

**問題**: Datadogダッシュボードでカスタムタグが表示されない

**解決策**:
1. `performRequest`関数を使用しているか確認
2. `tags`パラメータをHTTPリクエストに渡しているか確認
3. TestRun YAMLで環境変数(K6_TEST_RUN_ID, K6_ENV, K6_SCENARIO, K6_TEAM)を設定しているか確認
4. ConfigMap(k6-system-config)を参照しているか確認

### シナリオがクローンされない場合

**問題**: TestRun実行時にシナリオファイルが見つからない

**解決策**:
1. initContainersが正しく設定されているか確認
2. GitHub認証用Secret(k6-github-app-secrets)が存在するか確認
3. sparse-checkoutパターンが正しいか確認
4. ブランチ名(GITSYNC_REF)が正しいか確認

### メトリクスがDatadogに送信されない場合

**問題**: Datadogにメトリクスが表示されない

**解決策**:
1. ConfigMap(k6-system-config)を参照しているか確認
2. OTEL関連の環境変数が設定されているか確認
3. ネットワーク接続を確認

移行結果と効果

実際にGatlingシナリオをk6に移行した結果を記載します。サブエージェントを用いて移行したシナリオを実行したところ、問題なくマイクロサービスに想定リクエストが送信されていることを確認できました。Gatlingで実現していた以下の機能がk6でも同様に動作しています。

  • 環境変数による設定切り替え(エンドポイント、VU数、実行時間など)
  • CSVからのテストデータ読み込み
  • Datadogへのメトリクス送信とカスタムタグ付与

具体的な実装例を記載しただけでなく、チェックリストを使用したことでシナリオ移行の精度が上昇したと考えられます。

一方で、課題も発生しました。移行後のシナリオにて、不要なモジュールimport文を生成し、実行エラーになってしまうことがありました。

// 生成された不要なimport
import { open } from 'k6';

移行ガイドラインに「使用禁止のimportパターン」を明示的に追記することで解決しました。

- [ ] 不要な・存在しないimportは行わない
  - [ ] ex `import { open } from 'k6';`

サブエージェントを使う上でのポイント

今回の取り組みを通じて、シナリオ移行の精度が高いサブエージェントを実装できました。ポイントは以下のとおりです。

まず、ガイドラインの整備が最重要です。サブエージェントに渡すコンテキストの質が出力品質を直接左右します。具体例を多く含めることで変換精度が向上しました。そのため、ガイドラインの完成度をどの程度高くできるかがポイントとなります。

次に、チェックリストで品質を担保することです。LLMの出力にはばらつきがあるため、チェックリストで出力の精度を担保できたと考えています。特に「やってはいけないこと」を明示すると効果的に制限できました。

最後に、完全自動化ではなく半自動化として捉えることです。単純なシナリオであれば、サブエージェントの出力をほぼそのまま使用可能なことがわかりました。一方で、最終的なレビューや実際にシナリオを実行してみるなどの動作確認はまだまだ必要に思いました。

サブエージェントによるシナリオ移行は、手動での書き換えと比較して大幅な工数削減が期待できます。同様の移行を検討している方は、まず単純なシナリオから試し、ガイドラインを徐々に改善していくアプローチがおすすめです。

おわりに

本記事では、gatling-operatorのメンテナンス課題とk6導入時の工夫、シナリオ移行における生成AIの活用事例について紹介しました。

今回の負荷試験ツール移行では以下を実現しました。

  • 負荷試験ツールのメンテナンス負荷軽減
  • コンテナイメージビルドからの脱却によるシナリオ修正サイクルの改善
  • Datadogダッシュボードによる試験結果の可視化と分析強化
  • 生成AIを活用したシナリオ移行の効率化

今後は、残りのGatlingシナリオのk6への移行を進めつつ、Claudeサブエージェントへのプロンプトをさらに改善し、移行精度の向上に取り組んでいきたいと考えています。

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co


  1. 弊社のメトリクス保存期間は1年間です。永続的に残したい場合はダッシュボードのスクリーンショットで対応します。
カテゴリー