Keploy — eBPFでトラフィックを傍受しAPIテストを自動生成する仕組み
Go + eBPF + 透過プロキシでコード変更なしにテストを生成するCNCFプロジェクト
Goで書かれたCNCFプロジェクト。17k+スター。keploy recordを一度実行するだけで、アプリのAPI呼び出し、DBクエリ、外部リクエストを全て録画し、YAMLベースのテストケース+Mockとして出力する。
核心はeBPF。SDKのインストールもコード変更も不要。全ての言語が結局カーネルのsyscall(socket、connect、bind、sendmsg)を呼び出すので、そこをフックすれば言語に依存しない。
3層アーキテクチャ
Agentレイヤー — eBPFフック+透過プロキシ。カーネルに付着してトラフィックを傍受。
Serviceレイヤー — record、replay、mock、coverageオーケストレーション。録画/再生/比較のコアロジック。
Platformレイヤー — YAMLストレージ、言語別カバレッジ収集、Docker/Telemetry。
eBPFのトラフィック捕捉方法
pkg/agent/hooks/linux/にプリコンパイル済みeBPFオブジェクト(bpf_x86_bpfel.o、bpf_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/の透過プロキシがリダイレクトされたトラフィックを処理:
- eBPFリダイレクトされた接続をaccept
- eBPFマップから元の宛先を照会
- 最初のバイトでプロトコル判別(登録パーサーが
MatchType()呼び出し) - 該当プロトコルハンドラにルーティング
対応プロトコル:HTTP、HTTP2、gRPC、MySQL(ワイヤプロトコル全体 — 接続フェーズ、クエリ、prepared statements)、PostgreSQL、MongoDB、Redis、Kafka。Generic(バイナリキャプチャ)フォールバック。
TLSもpkg/agent/proxy/tls/で自動生成CA証明書により透過的に処理。
録画(Record)フロー
pkg/service/record/record.goがオーケストレーション:
Instrumentation.Setup()— eBPFフックロード、プロキシ起動Instrumentation.Run()— 対象アプリを起動- 3つの並行チャネルが開く:
GetIncoming()— インバウンドAPIリクエスト/レスポンス →TestCaseオブジェクトGetOutgoing()— アウトバウンド呼び出し(DB、外部API) →MockオブジェクトGetMappings()— どのMockがどのテストケースに属するか紐付け- YAMLで永続化。
testdb/にテストケース、mockdb/にMock。
- YAMLで永続化。
Goのgoroutineとchannelがこの並行パイプラインにぴったり合う構造。
再生(Replay)フロー
pkg/service/replay/がテスト実行:
- YAMLからテストケース+Mockをロード
- MockをプロキシのインメモリDB(
MockMemDb)に積載 - 録画されたHTTP/gRPCリクエストをアプリに送信
- アプリのアウトバウンド呼び出しがプロキシに到達→録画済みMockで応答
- 実際レスポンスvs期待レスポンスを比較
- デノイジング — タイムスタンプ、ランダムIDなどのノイズフィールドを自動識別して比較除外
- タイムフリージング — 再生時にシステム時間を固定
カバレッジ計算
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トラフィック捕捉→テスト生成フロー
ソースコード構造
| ディレクトリ | 役割 |
|---|---|
| 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共有マップ — リダイレクトの核心
| マップ名 | Key | Value | 用途 |
|---|---|---|---|
| clientRegistrationMap | app PID | 登録情報 | 対象アプリ識別 |
| agentRegistrationMap | agent PID | プロキシポート | Keployエージェント識別 |
| redirectProxyMap | ソースポート | DestInfo(IP+ポート) | 元の宛先保存 |
Go視点の設計ポイント
cilium/ebpfライブラリでeBPFプログラムをGoから直接ロード/管理。goroutine 3つがチャネルでインバウンド/アウトバウンド/マッピングを同時収集するパイプラインは、Go並行処理パターンの教科書的な使い方。インターフェースでプロトコルパーサーを抽象化し、新プロトコル追加はRecordOutgoing() + MockOutgoing() + MatchType()の実装のみで可能。
RubyからGoへ
eBPFがカーネルsyscall(socket/connect/bind)を傍受し、アプリの発信接続をKeployプロキシにリダイレクト
透過プロキシが先頭バイトでプロトコル判別→HTTP/MySQL/Postgres/MongoDB/Redis/Kafkaの各パーサーにルーティング
インバウンド(TestCase)+アウトバウンド(Mock)を3つのgoroutineチャネルで同時収集→YAML保存
再生時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レスポンスが実際と乖離しテストが壊れる。再録画が必要