この記事は DENSOアドベントカレンダー2020 の2日目の記事です。
今年は仕事でEnvoyのfilterを実装する機会があったのですが、まあそれ自体情報が少ないのと、使用する言語がC++、ビルドはbazel、テストはgoogle testという一般的なWEBエンジニアからすると馴染みのない環境でかなり苦労したので、今後挑戦する方のためにまとめておきます。
はじめに
そもそもEnvoyとは
自分が解説するよりもよい記事がいくらでもあるので「Envoyとは」でぜひググって頂きたいのですが、実態としてはNginxなどと同じOSSのプロキシです。主にサービスメッシュの文脈で語られる多い気がしますが、DropboxがNginxからEnvoyに移行したというブログを発表して話題になっていたりしましたね。
https://qiita.com/seikoudoku2000/items/9d54f910d6f05cbd556d
https://i-beam.org/2019/01/22/hello-envoy/
https://dev.classmethod.jp/articles/envoy-proxy-getting-started/
https://dropbox.tech/infrastructure/how-we-migrated-dropbox-from-nginx-to-envoy
Envoyのフィルターとは
Envoyのコア機能に影響を与えず、Pluggableにネットワークストリームのデータを操作できる機構です。Envoyのフィルターはリポジトリ内に大量に定義されていて、利用者はenvoy.yamlに定義することでこれらのフィルターを利用することが可能になっています。
https://github.com/envoyproxy/envoy/tree/237b29d6399953f22a47c6e4d19df74b4fbcee8d/source/extensions/filters/http
# こんな感じでつかっているはず
filter_chains:
- filter_chain_match:
transport_protocol: tls
filters:
- name: envoy.filters.network.sni_dynamic_forward_proxy
config:
port_value: 443
dns_cache_config:
name: dynamic_forward_proxy_cache_config
dns_lookup_family: V4_ONLY
- name: envoy.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
stat_prefix: tcp
また自作のフィルターを作って、Envoyに組み込むことも可能になっています。とすると何となく動的に自分の作ったフィルターを組み込めるような気がしますが、実際には自作のフィルターを含む形でEnvoy自体をビルドする必要があります。言い換えると自分のフィルターを組み込むには、自分専用にEnvoyを1からビルドする必要があるということです。
Envoyについて学ぶ
Envoy自体をそれほど使ったことがないという場合には、まずEnvoyの機能について一通り触ってみることをおすすめします。
Katacodaというサイトで無料でEnvoyの基本的な設定・操作について学べます。
こちらはGetting Startedなので、本当に基本的なことしかカバーされてないですが、合計で11個もコースがあるので興味があるものをいくつか試してみるとよいでしょう。
Getting started with Envoy | Katacoda
環境構築不要で、以下のようにサイト上からコンフィグを書いたり、実行したりできます。
その他のコースについてはこちらから
(https://www.katacoda.com/envoyproxy)
Envoyのフィルターについて学ぶ
日本語でEnvoyのカスタムフィルターの作り方についてまとまっている記事は多分ありません。。。 @tomoya-amachi さんの記事や、手前味噌ですが自分の記事を読んでいただくのが現在だと一番よいかなと思います。他にもよいものがあればコメントや編集リクエストで教えて下さい!
Envoyとして公式にフィルターを自作する場合のサンプルリポジトリも用意されているので、そちらを自分でビルドしてみて動作を確かめてみるのもよいかと思います。
https://github.com/envoyproxy/envoy-filter-example
またEnvoyはデフォルトでたくさんのフィルターを内蔵しているため、それらのコードを読むのも勉強になります。
https://github.com/envoyproxy/envoy/tree/master/source/extensions
Bazelについて学ぶ
EnvoyのビルドではBazelというGoogle製のツールが使われています。Envoyの中ではそんなに複雑なことはしていないと思うのですが、bazelは多機能でなかなか理解するのが難しいです。まずはC++のチュートリアルをやってみることをおすすめします。
https://docs.bazel.build/versions/master/tutorial/cpp.html
最初のとっかかりとして知っておくとよいのは以下です。
- 各ディレクトリがパッケージのように振る舞う
- 各ディレクトリに配置されたBUILDファイルにビルドの定義を書く
- bazel build //: のようにビルドする
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
例えば以下の http-filter-example
だと http-filter-example/
配下のBUILDファイルに独自フィルターを含めてEnvoyをビルドする設定が書かれています。
https://github.com/envoyproxy/envoy-filter-example/tree/master/http-filter-example
なのでビルドコマンドとしては bazel build //http-filter-example:envoy
になるわけです。
テストについて学ぶ
Google Test
Envoyのテストでは Google Test
というライブラリを使っています。マクロを多用しているので最初は少し読みにくいかもしれません。ただ機能としてはシンプルなので一度ドキュメントに目を通しておくことをおすすめします。
ちなみにEnvoyのFilterのテストは以下のような感じになっています。
// https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc#L64-L72
/**
* Requests that are _not_ header only, should result in StopIteration.
*/
TEST_F(AwsLambdaFilterTest, DecodingHeaderStopIteration) {
setupFilter({arn_, InvocationMode::Synchronous, true /*passthrough*/});
Http::TestRequestHeaderMapImpl headers;
const auto result = filter_->decodeHeaders(headers, false /*end_stream*/);
EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, result);
}
Envoyのフィルターのテスト
Envoyのフィルターのテストは実際にEnvoyにデフォルトで組み込まれているテストコードを読むのが一番です。自分はAWS Lambdaのフィルターのテストが丁度いい長さで参考になりました。
注意としてフィルターにはいくつかタイプがあり、networkなのか、HTTPなのかによってテストコードは結構変わってきます。networkフィルターのテストを書いているのにHTTPのフィルターのテストコードを参考にするとハマるので注意しましょう。
フィルターのテストは殆どの場合、以下で構成されています。インテグレーションテストがなかったり、コンフィグのテストがなかったりする場合もありますが、この3つのテストが基本的には書かれています。
- filter_test
- フィルター自体のユニットテスト。フィルターのもつ
decodeHeader
などのメソッドの戻り値や、意図したコールバック関数が呼ばれたかをチェックする
- フィルター自体のユニットテスト。フィルターのもつ
- config_test
- Yamlの文字列から意図したコンフィグの値にマッピングされているかどうか、不正なコンフィグの場合にエラーとなるかなどをテストする
- integration_test
- テスト対象のfilterを組み込んだ状態でEnvoyに対してリクエストを仮想的に送り、意図したレスポンスが帰ってくるか、意図したコールバック関数が呼ばれるかをテストする
一つ注意する必要があるのはモックの取り扱いです。
Google Testのモックはデフォルトでは明示的にASSERTしていないモックの関数呼び出しについてもワーニングを出すだけで許容してくれるのですが、EnvoyではStrictモードになっていて、それだけでテストがfail扱いになります。
Defaulting Envoy mocks to StrictMocks
テストは絶対に通ってるはずのになぜかテストがfailになる!!!となったら、このようなログが出ていないか確認してみましょう。
unknown file: Failure
Uninteresting mock function call - returning default value.
Function call: createFilesystemWatcher_()
自分はこれで1日くらい溶かしました…
CI
Envoyのビルドは性能のいいMBPでもクリーンビルドでは1時間弱かかります。テストもそれなりに時間がかかるので、CIで回す際には性能のインスタンスを使うように設定しましょう。自分のいたプロジェクトではGitLabが使われていたのですが、Envoyの場合には専用の性能のいいい独自Runnerが走るように設定してもらいました。
またbazelのキャッシュについても、適切に設定しないとまともな時間でテストを回すのが難しくなります。GitLabについては以下のドキュメントに詳しくまとまっているので参考にしてください。
https://about.gitlab.com/blog/2020/09/01/using-bazel-to-speed-up-gitlab-ci-builds/
まとめ
なかなか事前知識がないとハードルの高そうに見えるフィルターの自作ですが(実際高いけど…)、一度やってみるとネットワークの勉強にもなりますし、何より楽しいので、冬休みの自由研究に挑戦してみてはいかがでしょうか!