CloudFront Functionsのコードを変更するたびに、AWSへ持っていって確認するのが少しつらい。
できれば関数本体とテスト用のJSONをリポジトリへ置いて、普段のテストと同じようにローカルやCIでさっと実行したいと思っていました。
そこでCodexと相談しながら、CloudFront Functions向けのオフラインテストCLI cff-test を作ってもらいました。
何に困っていたか
CloudFront Functionsには、AWS上で関数を実行できる公式の TestFunction APIがあります。また、fujiwara/cfft を使えば、テストケースの管理や期待値との比較も便利に行えます。
一方で、どちらもAWS上のCloudFront Functionを使うため、AWS認証情報とネットワーク接続が必要です。
Node.jsからhandler(event)を直接呼ぶ方法ならローカルで完結しますが、CloudFront FunctionsのJavaScript runtime 2.0はNode.jsと同じ環境ではありません。使える構文や組み込みオブジェクトに違いがあり、ネットワーク、ファイルシステム、タイマーなどにも制限があります。
参考: CloudFront Functions JavaScript runtime 2.0の機能
つまり、欲しかったのは次の間を埋めるツールでした。
| 方法 | オフライン | Runtime互換性の確認 | 期待値との比較 |
|---|---|---|---|
| Node.jsの単体テスト | できる | できない | できる |
AWS TestFunction / cfft
|
できない | AWSのruntimeで実行 | できる |
cff-test |
できる | 対応範囲内で検査 | できる |
「これ、作れそうでは?」をCodexに相談した
最初は、次のようなCLIがあれば便利そう、というところから始めました。
test-tool function.js --event event.json --expected expected.json
Codexと壁打ちしてみると、CloudFront Functionsの完全なエミュレーターを作るのは難しいものの、実用的な互換性チェッカー兼ローカル実行環境なら現実的に作れそうだと分かりました。
方針はシンプルです。
- JavaScriptを静的解析し、runtime 2.0で使えない構文やAPIを検出する
- 組み込みのJavaScriptエンジンで
handler(event)を実行する - 戻り値を期待するJSONと比較する
配布やCIでの扱いやすさも考え、Rust製のシングルバイナリにする方針でCodexに実装を依頼しました。JavaScriptの実行にはQuickJSを組み込み、Node.jsを別途インストールしなくても動く構成になっています。
できたもの
cff-testでは、主に次の3つができます。
# runtime 2.0との互換性を静的検査
cff-test check function.js
# eventを渡して実行し、戻り値をJSONで表示
cff-test run function.js --event event.json
# 戻り値を期待値と比較
cff-test test function.js --event event.json --expected expected.json
たとえば、URIを書き換える関数をテストしてみます。
function handler(event) {
event.request.uri = "/rewritten";
return event.request;
}
CloudFront Functions形式の入力をevent.jsonへ、期待するrequestをexpected.jsonへ保存して実行します。
$ cff-test test function.js \
--event event.json \
--expected expected.json
PASS: function.js
期待値と違う場合はJSON Pointer単位で差分が出るので、どこが変わったかを確認できます。終了コードも成功時は0、互換性違反や期待値との差異がある場合は1になるため、そのままCIへ組み込めます。
Runtime 2.0向けの機能も少しずつ対応した
単にhandlerを呼ぶだけでなく、現在は次のような機能にも対応しています。
- viewer request / viewer responseのeventと戻り値の検証
- runtime 2.0で利用できない構文、global、module、memberの診断
-
crypto、querystring、Buffer - ローカルfixtureを使ったCloudFront KeyValueStore
-
--now-msによるDateの固定 - GitHub ActionsなどのCI環境での実行
特に時刻を固定できるのは、時刻に依存して振る舞いを変える関数のテストに便利でした。
cff-test test function.js \
--event event.json \
--expected expected.json \
--now-ms 0
KVSもAWS上のストアへ接続するのではなく、JSON fixtureを--kvsで渡します。関数コード、入力、期待値、KVSの状態をまとめてリポジトリで管理できるようになりました。
作ってみたら、欲しかった場所にうまく収まった
このツールの目的は、AWS上での最終確認をなくすことではありません。
pull request
↓
cff-testでローカル・CIテスト
↓
merge
↓
AWS TestFunctionやcfftで最終確認
↓
publish
日々の変更ではcff-testを使って素早くフィードバックを得て、本番runtimeで確認したい段階ではAWS上のテストを使う。この分担がちょうどよさそうです。
完全な再現を目指さず、対応範囲と既知の差異を明記した互換テスト層にしたことで、最初に欲しかった「AWSへつながず、ローカルやCIで気軽に回せるテスト」に収まりました。
Codexに作ってもらって感じたこと
今回、人間側で最初から細かな実装方法を決めていたわけではありません。
「CloudFront Functionsをローカルでテストしたい」から始めて、既存ツールとの違い、完全再現の難しさ、RustとGoのどちらが向いているか、と順番に壁打ちしました。そのうえで要件が固まったところから、実装とテストをCodexに進めてもらいました。
曖昧なアイデアでも、対話しながら実現可能なスコープまで削っていくと、思っていた以上にちゃんと使えるものになりました。特に、互換性のように「どこまで対応し、何を対象外にするか」が重要なツールでは、いきなりコードを書かせるより先に境界を決めたことが効いたと思います。
おわりに
cff-testは、AWSアカウントや認証情報、Node.jsなしでCloudFront FunctionsをテストできるCLIです。GitHub Releasesから単一の実行ファイルを取得するか、Rust環境があればソースからビルドできます。
対応範囲やインストール方法、GitHub Actionsでの使用例はREADMEにまとめています。
CloudFront Functionsの変更確認をもう少し手軽にしたい方は、試してもらえるとうれしいです。