🐛

Keploy — eBPFでトラフィックを傍受しAPIテストを自動生成する仕組み

Go + eBPF + 透過プロキシでコード変更なしにテストを生成するCNCFプロジェクト

Goで書かれたCNCFプロジェクト。17k+スター。keploy recordを一度実行するだけで、アプリのAPI呼び出し、DBクエリ、外部リクエストを全て録画し、YAMLベースのテストケース+Mockとして出力する。

核心はeBPF。SDKのインストールもコード変更も不要。全ての言語が結局カーネルのsyscall(socketconnectbindsendmsg)を呼び出すので、そこをフックすれば言語に依存しない。

3層アーキテクチャ

Agentレイヤー — eBPFフック+透過プロキシ。カーネルに付着してトラフィックを傍受。

Serviceレイヤー — record、replay、mock、coverageオーケストレーション。録画/再生/比較のコアロジック。

Platformレイヤー — YAMLストレージ、言語別カバレッジ収集、Docker/Telemetry。

eBPFのトラフィック捕捉方法

pkg/agent/hooks/linux/にプリコンパイル済みeBPFオブジェクト(bpf_x86_bpfel.obpf_arm64_bpfel.o)がある。cilium/ebpf Goライブラリでロード。ランタイムコンパイル不要。

主要フック:

sys_enter_socket tracepoint — ソケット作成syscallを捕捉して新規接続を追跡。

SockOps(cgroup付着) — cgroupv2パスに付着。cgroup内の全ソケット操作(connect、acceptなど)を監視するコアフック。

connect4/connect6 — IPv4/IPv6アウトバウンド接続を傍受。

cgBind4/cgBind6 — bind()呼び出しを監視してアプリのリッスンポートを把握。

BindEventsリングバッファ — eBPFがイベントをring bufferに書き込み、Goユーザスペースがringbuf.NewReaderで読み取る。

コアトリック:宛先リダイレクト

最も巧妙な部分。3つのeBPF共有マップが核心:

clientRegistrationMap — 対象アプリをeBPFに登録。
agentRegistrationMap — Keployエージェントを登録。
redirectProxyMap — ソースポート→元の宛先(DestInfo構造体:IPv4/IPv6+ポート)マッピング。

eBPFプログラムが発信接続の宛先をKeployのローカルプロキシ(127.0.0.1:proxyPort)に書き換える。元の宛先はredirectProxyMapに保存。プロキシはGetDestinationInfo(srcPort)で元の宛先を取得。

アプリからすれば普段通りDBに接続しているように見える。実際はKeployのプロキシを経由している。アプリコードは何も知らない。

透過プロキシのプロトコル解析

pkg/agent/proxy/の透過プロキシがリダイレクトされたトラフィックを処理:

  1. eBPFリダイレクトされた接続をaccept
  2. eBPFマップから元の宛先を照会
  3. 最初のバイトでプロトコル判別(登録パーサーがMatchType()呼び出し)
  4. 該当プロトコルハンドラにルーティング

対応プロトコル:HTTP、HTTP2、gRPC、MySQL(ワイヤプロトコル全体 — 接続フェーズ、クエリ、prepared statements)、PostgreSQL、MongoDB、Redis、Kafka。Generic(バイナリキャプチャ)フォールバック。

TLSもpkg/agent/proxy/tls/で自動生成CA証明書により透過的に処理。

録画(Record)フロー

pkg/service/record/record.goがオーケストレーション:

  1. Instrumentation.Setup() — eBPFフックロード、プロキシ起動
  2. Instrumentation.Run() — 対象アプリを起動
  3. 3つの並行チャネルが開く:
  • GetIncoming() — インバウンドAPIリクエスト/レスポンス → TestCaseオブジェクト

  • GetOutgoing() — アウトバウンド呼び出し(DB、外部API) → Mockオブジェクト

  • GetMappings() — どのMockがどのテストケースに属するか紐付け

    1. YAMLで永続化。testdb/にテストケース、mockdb/にMock。

Goのgoroutineとchannelがこの並行パイプラインにぴったり合う構造。

再生(Replay)フロー

pkg/service/replay/がテスト実行:

  1. YAMLからテストケース+Mockをロード
  2. MockをプロキシのインメモリDB(MockMemDb)に積載
  3. 録画されたHTTP/gRPCリクエストをアプリに送信
  4. アプリのアウトバウンド呼び出しがプロキシに到達→録画済みMockで応答
  5. 実際レスポンスvs期待レスポンスを比較
  6. デノイジング — タイムスタンプ、ランダムIDなどのノイズフィールドを自動識別して比較除外
  7. タイムフリージング — 再生時にシステム時間を固定

カバレッジ計算

pkg/platform/coverage/に言語別実装:Go、Java、Python、JavaScript、C#。各言語のネイティブカバレッジツール(Go cover、Istanbul、coverage.py、JaCoCoなど)にフック。

AI統合(utgen)

pkg/service/utgen/でLLMベースのテスト拡張。既存録画データ+Swagger/OpenAPIスキーマを読み込み、欠落フィールドテスト、境界値テスト、型エラーテストを生成。

Railsをラップするとどうなるか

keploy record -c "bundle exec rails s" — Railsがネットワーク経由でやることは全て録画される。

捕捉されるもの:

  • 受信HTTPリクエスト/レスポンス(APIエンドポイント)→テストケースになる

  • PostgreSQL/MySQLクエリ→Mockになる。ActiveRecordが内部で使うSQLがワイヤプロトコルレベルで全て捕捉される

  • Redis呼び出し(キャッシュ、Sidekiq)→Mock

  • 外部API呼び出し(Faraday、Net::HTTPなど)→Mock

  • Kafka/RabbitMQメッセージ→Mock

捕捉されないもの:

  • SQLite — ネットワークソケットを使わない。ファイルI/Oなので、eBPFネットワークフックに引っかからない

  • ファイル読み書き

  • インメモリ演算

  • localhost内プロセス間通信(Unixドメインソケットは一部対応)

核心:eBPFはネットワークソケットsyscallのみを捕捉する。TCP/UDPで通信するものは全て捕まり、それ以外は捕まらない。RailsでPostgreSQLを使っていれば、ActiveRecordが発行するSELECT/INSERT/UPDATEがPostgreSQLワイヤプロトコルで捕捉され、再生時は実DBなしにMockが応答する。

Linux専用 — macOS/Windows開発者の現実

eBPFはLinuxカーネル機能。macOSにはない。Windowsにもない(Keployが実験的にWindows用フックを試みているが未成熟)。

macOSではDockerが必須:

keploy record -c "docker compose up" --containerName "app"

Docker DesktopのLinux VM内でeBPFが動作する。ローカルネイティブ開発に慣れている場合、Dockerでラップする負担が増える。CI/CDでLinuxコンテナを使う分には問題ない。

eBPFトラフィック捕捉→テスト生成フロー

# 1. eBPFフックがカーネルに付着
sys_enter_socket, SockOps(cgroup), connect4/6, cgBind4/6
# 2. アウトバウンド接続先をプロキシにリダイレクト
App → connect(postgres:5432)
↓ eBPFが宛先を127.0.0.1:proxyPortに書き換え
↓ 元の宛先(postgres:5432)はredirectProxyMapに保存
App → connect(127.0.0.1:proxyPort) [アプリは知らない]
# 3. 透過プロキシがプロトコル解析
先頭バイト → MatchType() → PostgreSQLパーサー選択
リクエスト/レスポンスペアをMockオブジェクトとして捕捉
# 4. 並行収集(3 goroutineチャネル)
GetIncoming() ──→ TestCase(APIリクエスト/レスポンス)
GetOutgoing() ──→ Mock(DB/外部API)
GetMappings() ──→ TestCase ↔ Mock紐付け
# 5. YAML永続化
testdb/test-1.yaml + mockdb/mock-1.yaml

ソースコード構造

ディレクトリ役割
pkg/agent/hooks/linux/eBPFプログラム、マップ、カーネルフック
pkg/agent/proxy/透過プロキシ+DNSサーバー
pkg/agent/proxy/integrations/プロトコルパーサー(HTTP、MySQL、Postgres、Mongo、Redis、Kafka、gRPC)
pkg/service/record/録画オーケストレーション
pkg/service/replay/テスト実行+レスポンス比較
pkg/service/utgen/AI基盤テスト生成
pkg/matcher/レスポンス比較(HTTP、gRPC、スキーマ)
pkg/platform/coverage/言語別カバレッジ収集(Go/Java/Python/JS/C#)

eBPF共有マップ — リダイレクトの核心

マップ名KeyValue用途
clientRegistrationMapapp PID登録情報対象アプリ識別
agentRegistrationMapagent PIDプロキシポートKeployエージェント識別
redirectProxyMapソースポートDestInfo(IP+ポート)元の宛先保存

Go視点の設計ポイント

cilium/ebpfライブラリでeBPFプログラムをGoから直接ロード/管理。goroutine 3つがチャネルでインバウンド/アウトバウンド/マッピングを同時収集するパイプラインは、Go並行処理パターンの教科書的な使い方。インターフェースでプロトコルパーサーを抽象化し、新プロトコル追加はRecordOutgoing() + MockOutgoing() + MatchType()の実装のみで可能。

RubyからGoへ

1

eBPFがカーネルsyscall(socket/connect/bind)を傍受し、アプリの発信接続をKeployプロキシにリダイレクト

2

透過プロキシが先頭バイトでプロトコル判別→HTTP/MySQL/Postgres/MongoDB/Redis/Kafkaの各パーサーにルーティング

3

インバウンド(TestCase)+アウトバウンド(Mock)を3つのgoroutineチャネルで同時収集→YAML保存

4

再生時MockをインメモリDBに積載、録画リクエストをアプリに送信、デノイジングでタイムスタンプ等のノイズ除去後に比較

メリット

  • コード変更ゼロ — eBPFがカーネルで動作するためSDKやライブラリ不要
  • 言語非依存 — syscallを使う全言語(Go、Java、Python、Node、Rust等)で動作
  • プロトコルカバレッジが広い — HTTP/gRPC/MySQL/Postgres/MongoDB/Redis/Kafka+TLS透過処理

デメリット

  • Linux専用 — eBPFはカーネル5.15+必要。macOS/Windowsは必ずDocker内で実行が必要で、ローカルネイティブ開発ワークフローが崩れる
  • SQLite等の組み込みDBは捕捉不可 — ネットワークソケットを使わない通信はeBPFに引っかからない。PostgreSQL/MySQLのみ対応
  • 録画品質が実トラフィックに依存 — トラフィックがカバーしない経路にはテストもない。エラーケース、エッジケースは手動で作成が必要
  • デノイジングが完璧ではない — タイムスタンプ、ランダムID等の動的フィールド識別が失敗するとテストがflakyになる
  • 統合テストのみ生成 — ビジネスロジックの単体テストは作れない。「このメソッドが正しい値を返すか」の検証は依然手動で必要
  • DBスキーマ変更時に録画データが無効化 — カラム追加/削除でMockのSQLレスポンスが実際と乖離しテストが壊れる。再録画が必要

ユースケース

既存アプリにテストがない状態で、実トラフィックベースでリグレッションテストを素早く確保したい時 DB、外部API、メッセージキュー等の外部依存をMockで分離してオフラインテスト環境を作りたい時