eBPFを使った自動テストツール「Keploy」がすごい
この記事はKeployのバージョンv2.0.0-alpha53 を前提に執筆しております。
Keployとは
KeployはeBPFを利用して取得できるWebアプリケーションの通信に関するトレース情報を元に、テストとそのテストの実行時に利用するスタブサーバーを生成することができるツールとなります。
公式サイトのトップには以下のようなスローガンが掲げられています。
2 minutes to 90% test coverage!
テストに苦労した経験のある方は興味を惹かれるのではないでしょうか。
現在まだアルファ段階のプロジェクトですが、GitHubスター数は2683(2024/01/04現在)、CNCF Landscape にも掲載されているなど、一定の注目を集め始めているOSSです。
開発主体はプロダクトと同名のKeployというインド発のスタートアップで、去年GoogleによるインドのAIスタートアップ支援プログラムにも選定されたようです。
使用例
Keployの機能と特徴について理解するには、例を見るのがわかりやすいと思います。
以下では簡単なアプリケーションにテストを作成して実行する様子を示します。
使用したコードは以下で公開しています。
テスト対象
今回は例として以下のようなWeb APIを提供するnode.jsアプリケーションについて見ていきます。
dummyjson.com
からGETで取得してきたレスポンスをそのまま返却するアプリケーションです。
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を利用するに当たってアプリケーション側で必要となる唯一の変更です。
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 /app/ca.crt .
COPY /app/setup_ca.sh .
RUN apk add bash
###
COPY /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をベースイメージにしましょう。
以上私がつまづいたポイントでした。公式でサポート用のSlackも用意されているようですので、もしご自身でインストールされる際に困ったことがあればそちらで聞いてみるのも良いかもしれません。
まとめ
Keployの使い方や特徴を見てきました。ツールの魅力が少しでも伝わればよいなと思っています。
昨年末にeBPFについて調べていたときに偶然見つけたOSSでしたが、そのポテンシャルの高さに驚き、日本語での紹介も見当たらなかったため、拙いながらこのように記事にさせていただきました。
Web開発で利用できるeBPFによるツールは近年多く誕生しており、近い将来には新たなスタンダードとなるものも多いのではないかと感じております(個人的には同じくテストツールのhttps://tracetest.io/ にも注目しています。o11yに関するeBPFツールはたくさんありますが、既存のAPM製品と競合するものが多い気がしていて、それらと比べるとKeployやTracetestのような今までできなかったことを実現するツールに惹かれる)。Keployの今後の動向も含め、eBPFによってソフトウェア開発がどのように変わっていくのか継続的に見ていきたいと思います。
Discussion