🐰

eBPFを使った自動テストツール「Keploy」がすごい

2024/01/06に公開

この記事はKeployのバージョンv2.0.0-alpha53 を前提に執筆しております。

Keployとは

https://keploy.io/

KeployはeBPFを利用して取得できるWebアプリケーションの通信に関するトレース情報を元に、テストとそのテストの実行時に利用するスタブサーバーを生成することができるツールとなります。
公式サイトのトップには以下のようなスローガンが掲げられています。

2 minutes to 90% test coverage!

テストに苦労した経験のある方は興味を惹かれるのではないでしょうか。

現在まだアルファ段階のプロジェクトですが、GitHubスター数は2683(2024/01/04現在)、CNCF Landscape にも掲載されているなど、一定の注目を集め始めているOSSです。
開発主体はプロダクトと同名のKeployというインド発のスタートアップで、去年GoogleによるインドのAIスタートアップ支援プログラムにも選定されたようです

使用例

Keployの機能と特徴について理解するには、例を見るのがわかりやすいと思います。
以下では簡単なアプリケーションにテストを作成して実行する様子を示します。
使用したコードは以下で公開しています。
https://github.com/epli2/keploy-minimal-example

テスト対象

今回は例として以下のようなWeb APIを提供するnode.jsアプリケーションについて見ていきます。
dummyjson.comからGETで取得してきたレスポンスをそのまま返却するアプリケーションです。

index.mjs
import express from "express";
const app = express();

app.get("/products/:productId", async (req, res) => {
  const productId = req.params.productId;
  const product = await (await fetch(`https://dummyjson.com/products/${productId}`)).json();
  res.send(product);
});

app.listen(3000);

このアプリケーションをコンテナに詰めて実行し、それをテスト対象として考えましょう。
以下のDockerfileはマルチステージビルドのnodejsイメージを作成するシンプルなパターンに対し、Keployのドキュメント に従って証明書のインストール処理を追加しています。
この証明書のインストールはKeployを利用するに当たってアプリケーション側で必要となる唯一の変更です。

Dockerfile
FROM node:20.10-alpine3.19 as builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

### アプリケーションが行うTLS通信をKeployで読み取れるように証明書をインストールしておく必要がある
### 証明書とインストール用スクリプトのダウンロード
RUN apk add curl
RUN curl -o ca.crt https://raw.githubusercontent.com/keploy/keploy/main/pkg/proxy/asset/ca.crt
RUN curl -o setup_ca.sh https://raw.githubusercontent.com/keploy/keploy/main/pkg/proxy/asset/setup_ca.sh
RUN chmod +x setup_ca.sh
###

FROM node:20.10-alpine3.19

WORKDIR /app

### 証明書とインストール用スクリプトのコピー
COPY --from=builder /app/ca.crt .
COPY --from=builder /app/setup_ca.sh .
RUN apk add bash
###

COPY --from=builder /app/node_modules ./node_modules
COPY index.mjs .

EXPOSE 3000

## 証明書インストール用スクリプトとアプリケーションを実行
CMD ["/bin/bash", "-c", "source ./setup_ca.sh && node /app/index.mjs"]

テスト作成

テストの作成は非常に簡単です。

Keployを噛ませてdocker runして

keploy record -c "docker run --name keploy-minimal-example -p 3000:3000 --network keploy-network keploy-minimal-example" --containerName "keploy-minimal-example"

curl等で叩いてあげるだけで

curl localhost:3000/products/1

以下のように、Keployで実行できるテストケース(test-1.yaml)とスタブの定義(mocks.yaml)が生成されます。

🐰 Keploy: 2024-01-02T13:34:29Z         INFO    yaml/yaml.go:219        🟠 Keploy has captured test cases for the user's application.   {"path": "/files/keploy/test-set-0/tests", "testcase name": "test-1"}
% tree keploy
keploy
└── test-set-0
    ├── mocks.yaml
    └── tests
        └── test-1.yaml

他のテストケースを作るには、そのようにリクエストするだけです。

curl localhost:3000/products/2

もう一つのテストケース(test-2.yaml)が作成されました

% tree keploy
keploy
└── test-set-0
    ├── mocks.yaml
    └── tests
        ├── test-1.yaml
        └── test-2.yaml

なんとテスト作成の手順はこれだけです。一行もコードを書いていないのに、テストケースとスタブが完成してしまいました。

テスト実行

完成したテストケースとスタブを実行して、アプリケーションをテストしてみましょう。
テスト作成の際と同様に、コマンドは以下の一行を実行するだけです。これだけでKeployはアプリケーションからの外部への通信を自動的にスタブに差し替えてくれるため、アプリケーション側でのコードや設定変更が必要ありません。

keploy test -c "docker run --name keploy-minimal-example -p 3000:3000 --network keploy-network keploy-minimal-example" --containerName "keploy-minimal-example" --delay 5

実行結果は以下のようになりました。

% keploy test -c "docker run --name keploy-minimal-example -p 3000:3000 --network keploy-network keploy-minimal-example" --containerName "keploy-minimal-example" --delay 5
2024/01/03 16:30:22 must use ASL logging (which requires CGO) if running as root
latest: Pulling from keploy/keploy
Digest: sha256:642b403fc6cc691679c78b5c6803fa01e07d5f36583b6fb6b4e49d677b8aa7f8
Status: Image is up to date for ghcr.io/keploy/keploy:latest

       ▓██▓▄
    ▓▓▓▓██▓█▓▄
     ████████▓▒
          ▀▓▓███▄      ▄▄   ▄               ▌
         ▄▌▌▓▓████▄    ██ ▓█▀  ▄▌▀▄  ▓▓▌▄   ▓█  ▄▌▓▓▌▄ ▌▌   ▓
       ▓█████████▌▓▓   ██▓█▄  ▓█▄▓▓ ▐█▌  ██ ▓█  █▌  ██  █▌ █▓
      ▓▓▓▓▀▀▀▀▓▓▓▓▓▓▌  ██  █▓  ▓▌▄▄ ▐█▓▄▓█▀ █▓█ ▀█▄▄█▀   █▓█
       ▓▌                           ▐█▌                   █▌
        ▓
  
version: 2.0.0-alpha53

🐰 Keploy: 2024-01-03T07:30:23Z         WARN    cmd/test.go:216 Delay is set to 5 seconds, incase your app takes more time to start use --delay to set custom delay
🐰 Keploy: 2024-01-03T07:30:23Z         INFO    cmd/test.go:218 Example usage: keploy test -c "docker run -p 8080:8080 --network myNetworkName myApplicationImageName" --delay 6
🐰 Keploy: 2024-01-03T07:30:23Z         WARN    cmd/test.go:225 buildDelay is set to 30s, incase your docker container takes more time to build use --buildDelay to set custom delay
🐰 Keploy: 2024-01-03T07:30:23Z         INFO    cmd/test.go:226 Example usage:keploy test -c "docker-compose up --build" --buildDelay 35s
🐰 Keploy: 2024-01-03T07:30:23Z         INFO    cmd/test.go:250         {"keploy test and mock path": "/files/keploy", "keploy testReport path": "/files/keploy/testReports"}
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    hooks/loader.go:786     keploy initialized and probes added to the kernel.
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    proxy/proxy.go:437      Keploy has hijacked the DNS resolution mechanism, your application may misbehave in keploy test mode if you have provided wrong domain name in your application code.
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    proxy/proxy.go:451      Proxy started at port:16789
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    test/test.go:346        running user application for    {"test-set": "test-set-0"}
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    proxy/proxy.go:608      starting DNS server at addr :16789
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    hooks/launch.go:557     trying to inject network:keploy-network to the keploy container
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    hooks/launch.go:595     Successfully injected network to the keploy container   {"Keploy container": "keploy-v2", "appNetwork": "keploy-network"}
Java is not installed on the system
NODE_EXTRA_CA_CERTS is set to: /tmp/ca.crt
REQUESTS_CA_BUNDLE is set to: /tmp/ca.crt
Setup successful
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    hooks/launch.go:438     container & network found and processed successfully    {"time": 1704267026933290180}
🐰 Keploy: 2024-01-03T07:30:26Z         INFO    test/test.go:396                {"no of test cases": 2, "test-set": "test-set-0"}
🐰 Keploy: 2024-01-03T07:30:31Z         INFO    pkg/util.go:65  starting test for of    {"test case": "test-1", "test set": "test-set-0"}
Testrun passed for testcase with id: "test-1"

--------------------------------------------------------------------

🐰 Keploy: 2024-01-03T07:30:32Z         INFO    test/test.go:435        result  {"testcase id": "test-1", "testset id": "test-set-0", "passed": "true"}
🐰 Keploy: 2024-01-03T07:30:32Z         INFO    pkg/util.go:65  starting test for of    {"test case": "test-2", "test set": "test-set-0"}
Testrun passed for testcase with id: "test-2"

--------------------------------------------------------------------

🐰 Keploy: 2024-01-03T07:30:32Z         INFO    test/test.go:435        result  {"testcase id": "test-2", "testset id": "test-set-0", "passed": "true"}
🐰 Keploy: 2024-01-03T07:30:32Z         INFO    test/test.go:515        test report for test-set-0:     {"name: ": "report-5", "path: ": "/files/keploy/report-5"}

 <=========================================> 
  TESTRUN SUMMARY. For testrun with id: "test-set-0"
        Total tests: 2
        Total test passed: 2
        Total test failed: 0
 <=========================================> 

🐰 Keploy: 2024-01-03T07:30:32Z         INFO    hooks/loader.go:365     keploy has initiated the shutdown of the user application.
🐰 Keploy: 2024-01-03T07:30:42Z         WARN    hooks/launch.go:534     userApplication has exited with exit code: 137
🐰 Keploy: 2024-01-03T07:30:42Z         INFO    test/test.go:353        keploy terminated user application
🐰 Keploy: 2024-01-03T07:30:42Z         INFO    test/test.go:253        test run completed      {"passed overall": true}
🐰 Keploy: 2024-01-03T07:30:42Z         INFO    hooks/loader.go:421     Exiting keploy program gracefully.
🐰 Keploy: 2024-01-03T07:30:43Z         INFO    hooks/loader.go:471     eBPF resources released successfully...
🐰 Keploy: 2024-01-03T07:30:43Z         INFO    proxy/proxy.go:1023     Dns server stopped
🐰 Keploy: 2024-01-03T07:30:43Z         INFO    proxy/proxy.go:1025     proxy stopped...

アプリケーション起動後、作成したふたつのテストが成功し、シャットダウンして終了していることがわかります。

Keployのここがすごい

ここまでの例だけでも、以下のことがわかります。

  • 完全にノーコードでテストの作成と実行が実現できる
  • アプリケーションがどのような外部サービスに依存しているか事前に把握していなくても、それをスタブしてテストを作成できる
  • テスト実行時に外部APIをスタブサーバーへ明示的に差し替える必要がない

さらに、例では紹介しきれていない以下の特徴や機能も持っています。

  • eBPFベースであるため実装言語を問わず使用できる
  • HTTPだけでなく、データベース(Postgres、MySQL、MongoDB、Redis)とgRPCサーバーのスタブ作成機能あり
  • go-test、JUnit、Pytest、Jestから利用するためのSDKが提供されているため、既存のカバレッジ取得ツールと容易に組み合わせられる
  • テストとスタブ両方で、Timestampなどの毎回変更があるフィールド(=noise)を自動的に無視する機能あり

これらの特徴や機能を総合して考えると、冒頭で紹介したスローガンである2 minutes to 90% test coverage!も大袈裟な話ではないのかもしれないと思いました。
Keployをバインドしたアプリケーションをデプロイし、既存の手動テストシナリオを実行すればそれだけで自動テストが完成しますし、テスト時には外部通信がすべてスタブ化されるため、テスト自体の安定性も高いと想像できます。
アプリケーション全体の実行が必須となるため、実行オーバーヘッドの関係からユニットテストを完全に置き換えることはないと思いますが、うまく棲み分けることで効率的なテストを実現できそうです。
また、実装言語を問わず使用できるため、システムの開発言語やフレームワークを変更する際のテストなどでは特に強力なツールになるのではないかと思います。
加えて、まだ実現されていない機能としてパフォーマンステストの実行があるのですが、これも実現すれば性能面でのリグレッションテストを容易に実施できそうで、期待したいところです。

Keployのインストール

Keployを使ってみたくなった方もいるのではないでしょうか。
https://keploy.io/docs/ で各環境へのインストール方法が説明されているので、基本的にはそちらを読んでいただきたいですが、まだアルファバージョンということもあり、いくつかつまづくポイントがあったので、私が試したApple SiliconのMacでの導入手順を書いておきます。

Apple Silicon Macでのインストール

公式ドキュメントでは、以下のコマンドを実行するだけとあります。

curl -O https://raw.githubusercontent.com/keploy/keploy/main/keploy.sh && source keploy.sh

つまづきポイント①

私のMacではIntel Macから移行ユーテリティで環境をコピーしていた関係でHomebrewがIntel版になっていたため、インストール後うまく動作しませんでした(どういうエラーが出たかは忘れてしまいました)。
Apple Silicon Macをお使いの方は、which brewの結果が/opt/homebrew/bin/brewでない場合はarm版のHomebrewをインストールしてパスを通してからインストールコマンドを実行してください。
(こちらの記事にお世話になりました。 https://zenn.dev/omakazu/scraps/b3a4be96741a22)

つまづきポイント②

上記のインストールコマンドを実行すると対話形式で以下のように質問されます。Dockerを使ったインストールができそうに見えます。しかし私の環境ではうまく動作しませんでした。Colimaを選択するのが良さそうです。 (追記: Dockerバージョン4.25.2以降であれば動作可能なようです)
Do you want to install keploy with Docker or Colima? (docker/colima):

つまづきポイント③

ドキュメントには以下のようにエイリアスを作るように書かれていますが、この通り設定するとkeployコマンドが途中で終了してしまうなど不安定でした

alias keploy='sudo docker run --pull always --name keploy-v2 -p 16789:16789 --network keploy-network --privileged --pid=host -it -v "$(pwd)":/files -v /sys/fs/cgroup:/sys/fs/cgroup -v /sys/kernel/debug:/sys/kernel/debug -v /sys/fs/bpf:/sys/fs/bpf -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/keploy/keploy'

私は-tオプションを削って以下のようにし、~/.zshrcに追加しています。

alias keploy='sudo docker run --pull always --name keploy-v2 -p 16789:16789 --network keploy-network --privileged --pid=host -i -v "$(pwd)":/files -v /sys/fs/cgroup:/sys/fs/cgroup -v /sys/kernel/debug:/sys/kernel/debug -v /sys/fs/bpf:/sys/fs/bpf -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/keploy/keploy'

(この問題は後でissue立てておこうと思っています)

つまづきポイント④

③まででKeploy自体のインストールはなんとかこなせるのではないかと思います。
最後に、アプリケーションコンテナの設定でつまづきました。
既知のバグとして、アプリケーションのコンテナが古いDebianの場合keploy testがうまく動作しないというものがあり、私はnode:18.19-bullseyeをベースイメージとして使おうとしてこれを踏みました。バグが修正されるまでは、新しいDebianかalpineなど他のOSをベースイメージにしましょう。
https://github.com/keploy/keploy/issues/645

以上私がつまづいたポイントでした。公式でサポート用のSlackも用意されているようですので、もしご自身でインストールされる際に困ったことがあればそちらで聞いてみるのも良いかもしれません。

まとめ

Keployの使い方や特徴を見てきました。ツールの魅力が少しでも伝わればよいなと思っています。
昨年末にeBPFについて調べていたときに偶然見つけたOSSでしたが、そのポテンシャルの高さに驚き、日本語での紹介も見当たらなかったため、拙いながらこのように記事にさせていただきました。
Web開発で利用できるeBPFによるツールは近年多く誕生しており、近い将来には新たなスタンダードとなるものも多いのではないかと感じております(個人的には同じくテストツールのhttps://tracetest.io/ にも注目しています。o11yに関するeBPFツールはたくさんありますが、既存のAPM製品と競合するものが多い気がしていて、それらと比べるとKeployやTracetestのような今までできなかったことを実現するツールに惹かれる)。Keployの今後の動向も含め、eBPFによってソフトウェア開発がどのように変わっていくのか継続的に見ていきたいと思います。

Discussion