Flatt Security Blog

株式会社Flatt SecurityのBlogです。

Proxy-Wasm + Rust による Envoy の拡張 ―― 独自メトリクスの追加を例に

f:id:flattsecurity:20210113203116p:plain This image includes the work that is distributed in the Apache License Version 2.0

株式会社Flatt Securityでセキュリティエンジニアとして働いている米内です。 本稿では、Envoy に独自メトリクスを追加するようなフィルタのサンプル実装の提示を通して、 WebAssembly によるサービスプロキシの拡張を可能にする仕様である Proxy-Wasm について紹介します。

Proxy-Wasm とは

近年は WebAssembly System Interface(WASI) を中心として、「WebAssembly は可搬生や速度面の利点があるのだから、Web ブラウザの外でも活用しよう」というような動きが各所で発生しています。 Web Assembly for Proxies(Proxy-Wasm) もその一つであり、この仕様は、プロキシサーバと(WebAssembly バイナリとして動作する)その拡張機能との間の ABI を定義するものです。

例えば、近年用例が非常に増えてきたプロキシサーバである Envoy の拡張機能の開発には、少し前までは C++ が必須でした。 また、Envoy のビルドコストは決して小さくありません(試しにビルドしてみると分かります)。 これらは Envoy に拡張機能を追加したい開発者にとって、一定のハードルとなっていました。 実際、弊社は Flatt Security Learning Platform 内で部分的に Envoy を利用しているのですが、メンテナンスコストがネックとなり Envoy の拡張機能の開発を避けてきました。

一方、現在の Envoy の master ブランチの実装は、既に現状の Proxy-Wasm の仕様に対応しています1。 そのため現在は、Envoy の拡張機能を作成する際、Proxy-Wasm の SDK が提供されている言語(Rust, TinyGo, AssemblyScript, C++)の中から好きなものを選択することができます。 拡張機能の開発の際にいちいち Envoy をビルドしてやる必要もありません。 また、Proxy-Wasm の仕様に対応しているプロキシであれば2、原理上は一度作成した拡張機能を使い回せることになります。 特定のソフトウェアに依存しすぎずに拡張機能が書けるというのは、運用上の安心感にも寄与してくれそうです。

Proxy-Wasm の周辺

Proxy-Wasm 周辺のエコシステムは日々進化しつつあります。 OCI Image として Proxy-Wasm に基づくプロキシ拡張をバンドルする方法を定義する WASM Artifact Image Specification という仕様が Solo.io や Google、Istio コミュニティを中心に策定が進められています。 また、実際に拡張機能を共有するための WebAssembly Hub なるプラットフォームも存在しています。 同プラットフォームの CLI クライアントとも呼べる wasme の開発も進められています3

また、このような流れの中、表立った実用例も少しずつ増えてきています。 例えば Istio の upstream には既に Proxy-Wasm を用いた実装 が存在しています。 また、昨年後半に開催された EnvoyCon 2020 では、Verizon Media における Proxy-Wasm の利用例 が報告されています。

Rust による Envoy の拡張

せっかくなので、Proxy-Wasm に基づいた Envoy の拡張機能の Rust による実装例を用意してみました。

この実装は、Proxy-Wasm で定義されているメトリクス系の API を用いて、Envoy に以下の 2 つの独自メトリクスを追加する、というものになっています。

  • stat_filter.custom_metric.num_of_request … 拡張機能を通過した HTTP リクエストの数
  • stat_filter.custom_metric.num_of_response … 拡張機能を通過した HTTP レスポンスの数

私が知る限り、メトリクス系 API を利用している参考実装はそう多くありませんから、今回の実装例は他の実装例を見たことがある方にとっても一見の価値のあるのではないかと思います。

ここから本実装の肝となる部分をかいつまんで紹介していきます。

SDK の利用

Rust を用いて Envoy の拡張機能を開発する場合には、proxy-wasm-rust-sdk という SDK を利用することができます。Proxy-Wasm に基づく拡張機能の開発に必要な準備は、この SDK を Cargo.toml 内の dependencies に追加し、ビルドターゲットを wasm32-unknown-unknown としてやることだけです。

なお、proxy-wasm-rust-sdk に基づいて作られた envoy-wasm-rust-sdk を利用すると、よりリッチなインターフェースの上で開発を進めることができます。今回の例示ではできるだけプリミティヴな部分を残したかったため利用していませんが、もし新しく拡張機能を開発するのであれば、利用を検討してみてください。

スタートアップ処理

Proxy-Wasm では _start というシンボルが拡張機能のロード時のエントリポイントとして取り扱われます。 今回の実装の以下の箇所 では、 set_root_context という関数を用いて、SampleFilterRoot を拡張機能として登録しています。

#[no_mangle]
pub fn _start() {
    proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
        Box::new(SampleFilterRoot {
            req_metric_id: 0,
            resp_metric_id: 0,
        })
    });
}

メトリクスの定義

SampleFilterRoot に実装されている RootContext トレイトは、拡張機能自体の振る舞いを定義しています。 とくにこのトレイトに含まれている関数 on_vm_start は、当該拡張機能のための WebAssembly Virtual Machine が開始した段階で呼ばれる関数です。 今回の実装では、この中で Proxy-Wasm ABI 内の proxy_define_metric 関数を呼び出すことにより、新しいメトリクスを定義しています。

具体的には 以下の部分 において、stat_filter.custom_metric.num_of_request という名前のカウンターが定義されています。

        self.req_metric_id = match proxy_wasm::hostcalls::define_metric(
            MetricType::Counter,
            "stat_filter.custom_metric.num_of_request",
        ) {
            Ok(metric_id) => metric_id,
            Err(e) => panic!("Error: {:?}", e),
        };

proxy_wasm::hostcalls::define_metric 関数の返り値は、新しく定義したメトリクスを利用するために必要な ID です。 ここでは SampleFilterRoot 構造体のメンバに値を退避しています。

メトリクスの値の変更

SampleFilterRoot が実装している RootContext トレイト内の create_http_context 関数の中では、HttpContext トレイトを実装した構造体からなる値が返却されています。これにより、L7 の通信がフックされた場合のハンドラが登録されています。

実際に HTTP 通信が拡張機能によりフックされると、それが HTTP リクエストである場合は、その都度 以下の処理 が発火します。 この処理では stat_filter.custom_metric.num_of_request の値がインクリメントされています。

    fn on_http_request_headers(&mut self, _: usize) -> Action {
        proxy_wasm::hostcalls::increment_metric(self.req_metric_id, 1).unwrap();
        Action::Continue
    }

一方、HTTP レスポンスがフックされた場合には、以下の処理 が発火します。 この処理では stat_filter.custom_metric.num_of_response の値がインクリメントされています。

    fn on_http_response_headers(&mut self, _: usize) -> Action {
        proxy_wasm::hostcalls::increment_metric(self.resp_metric_id, 1).unwrap();
        Action::Continue
    }

動作例

まずは新しく定義したメトリクスが確かに Envoy の Administration interface から取得できることを確かめてみましょう。 今回紹介した拡張機能の実装が有効化された Envoy は、以下のコマンドにより起動することができます。

$ git clone git@github.com:lmt-swallow/proxy-wasm-custom-metrics-example.git
(snip ...)
$ cd proxy-wasm-custom-metrics-example; docker-compose up --build
(snip ...)

Envoy が起動できたら、localhost:9901 で Listen している Envoy の Administration interface の /stats エンドポイントにリクエストしてみましょう。すると以下のように、名前が stat_filter.custom_metric から始まる 2 つのメトリクスが存在していることが確認できます。確かに新しいメトリクスが定義できていますね。

$ curl -s localhost:9901/stats | grep stat_filter
stat_filter.custom_metric.num_of_request: 0
stat_filter.custom_metric.num_of_response: 0

メトリクスの存在が確認できたら、localhost:8080 で Listen している Envoy に対して HTTP リクエストを送信してみましょう。 すると、以下の実行例のように、stat_filter.custom_metric から始まる 2 つのメトリクスの値が確かにインクリメントされていることが確認できます。メトリクスの値の変更もうまく動作していそうです。

$ curl http://localhost:8080
hello from mock
$ curl -s localhost:9901/stats | grep stat_filter
stat_filter.custom_metric.num_of_request: 1
stat_filter.custom_metric.num_of_response: 1

以上のようなステップにより、Proxy-Wasm を用いた拡張機能から Envoy に独自メトリクスが追加できていることが確認できました。 たったの 69 行の Rust コードで、特に Envoy 本体のソースコードに触れることなく拡張機能が作成できるのは嬉しいですね。

おわりに

本稿では、独自メトリクスを定義する Envoy 拡張機能のサンプル実装を紹介しながら、Proxy-Wasm なる仕様について紹介しました。Proxy-Wasm はまだ若い仕様ですが、現状でも「Istio で構成された Service Mesh に対して、Rust や TinyGo で書いた小さなフィルタを動的に注入する」といったことが簡単にできるような世界観になっています4。既に十分に試し甲斐のある領域まで来ている、といってよいでしょう。

なお、日本語の情報の中では、Infra Study Meetup #8 での Takeshi Yoneda 氏の発表資料 や、同氏の WebAssembly Night #10 での発表資料 が非常によく整理されています。 さらなる背景や技術的詳細が気になる方は、ぜひこれらの資料を起点として引き続き情報を収集してみてください。

弊社(株式会社Flatt Security)は、Flatt Security Learning Platform を始めとした新規プロダクトを一緒に推進していけるメンバーを募集しています。 ご興味のある方は、弊社Webサイトの採用情報ページからお気軽にお問い合わせください。


  1. まだ Stable なリリースの中には含められていません。詳しくは envoyproxy/envoy #4272 を参照して下さい。

  2. 現状では Envoy と Apache Traffic Server が対応しているようです。

  3. 実際は WebAssembly Hub の CLI クライアント以上の機能を備えています。詳しくは wasme help コマンドの実行結果を参照してください。

  4. Istio で構築したサービスメッシュにデプロイするときは、wasme コマンドを利用してOCI イメージの形になっている拡張機能を配布するのが最も簡単だと思います。このコマンドを利用すると、内部的には Wasme Operator の DaemonSet がクラスタ内に設置されます。その後、それらがプロキシ拡張の OCI イメージを Pull し、同イメージが各 Sidecar Proxy(Envoy)に hostPath によりマウントされてくれます。なお、この辺の挙動は現状微妙にバギーなので、注意が必要です。