🏇

Rust × WebAssembly(Wasm) × Edge処理 Google Cloudで「サービス拡張」を試してみた

に公開

はじめに

Rust × WebAssembly(Wasm) × Edge処理を試してみました。

WebAssemblyのプロジェクトの1つに、Web Assembly for Proxies(Proxy-Wasm)で、通信(Envoy)に拡張機能を追加できます。
そのProxy-Wasmが、Google Cloudの「サービス拡張」として組み込まれ、Edge処理で使えます。
多数のサンプル(Rust、Go、C++)が公開されているため、使ってみることにしました。

目標としては、Rustを使って簡易なBasic認証の実装です
Authorizationヘッダーをチェックする認証を行います。ヘッダーが存在しない、または不正な場合は、401 Unauthorizedレスポンスを返します。

Google Cloudのセットアップは終わっている前提で進めます。

Google Cloudの「サービス拡張」をセットアップ

「サービス拡張」のドキュメントを見ながら進めます。
https://cloud.google.com/service-extensions/docs/plugins-overview

サービスを作る

まずは、ドキュメントを参考にしつつ、Rustのコードを作ります。

ドキュメント
https://cloud.google.com/service-extensions/docs/prepare-plugin-code

Rustを未導入の方は、インストールしてください。
https://www.rust-lang.org/tools/install

まず、Rustのtoolchainをインストールします。

rustup target add wasm32-wasip1

それでは、プロジェクトを作ります。

cargo new --lib proxy-wasm-plugin-basic-auth

ベースとなるファイルが出力されました。
ここをプロジェクトフォルダとして進めます。

cd proxy-wasm-plugin-basic-auth

Rustのコードを書く

ファイルの中の /src/lib.rsがプログラムの本体です。

プログラムを作るには、

  1. HTTP リクエスト ヘッダーを処理するためのコールバックを特定
  2. Authorizationヘッダーをチェックし、認証が不正の時はアクセスを拒否するリクエストを返す
  3. 成功時は、Authorizationヘッダーを削除してリクエストを続行する(一部のGoogle Cloudのサーバは、Authorizationヘッダーがあるとアクセスできない場合があるため)

を実装する必要があります。

ドキュメントによると、HttpContext::on_http_request_headers がHTTP リクエスト ヘッダーを処理するためのコールバックです。
https://cloud.google.com/service-extensions/docs/prepare-plugin-code#callbacks

このコールバックを実装して、Authorizationヘッダーをチェックし、認証を行います。

ヘッダーを元にリクエストを拒否するコードとしては以下のサンプルが参考になります。
https://github.com/GoogleCloudPlatform/service-extensions/blob/main/plugins/samples/block_request/plugin.rs

レスポンスを生成して返すことができることがわかります。

403レスポンスを返す
self.send_http_response(
    403,
    vec![],
    Some(format!("Forbidden - Request ID: {}", request_id).as_bytes()),
);

ヘッダーの削除については、こちらも参考にしました
https://b-nova.com/en/home/content/build-your-custom-envoy-http-filter-with-proxy-wasm/

ヘッダーの削除
self.set_http_request_header("Authorization", None);

実行速度を速めるため、Basic認証のID/PWを事前にBase64エンコードしておきます。

(bash or WSL)コマンドで、ID/PWをBase64エンコードの例
echo -n "user:password" | base64

これらを組み合わせて、/src/lib.rsに以下のようなコードを書きました。

proxy-wasm-plugin-basic-auth
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use log::info;

proxy_wasm::main! {{
    proxy_wasm::set_log_level(LogLevel::Trace);
    proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(MyHttpContext) });
}}

struct MyHttpContext;

impl Context for MyHttpContext {}


// 認証コードサンプル(ID: user / PW: password)
const AUTH_STRING: &str = "Basic dXNlcjpwYXNzd29yZA==";

// ベーシック認証
impl HttpContext for MyHttpContext {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {

        // Authorizationヘッダーが存在し、かつ正しい場合はリクエストを続行します。
        if let Some(authorization) = self.get_http_request_header("Authorization") {
            if authorization == AUTH_STRING {
                // Authorizationヘッダーを無条件に削除します
                self.set_http_request_header("Authorization", None);
                return Action::Continue;
            }
        }

        // Authorizationヘッダーが存在しない、または不正な場合は、401 Unauthorizedレスポンスを生成します。
        self.send_http_response(
            401,
            vec![
                ("WWW-authenticate", "Basic realm=\"Secure Area\""),
            ],
            Some(format!("Unauthorized").as_bytes()),
        );
        
        info!("Forbidden request: Authorization header missing or invalid.");

        return Action::Pause;
    }
}

Cargo.tomlの設定

次に、フォルダ直下 Cargo.tomlを編集して、必要な依存関係を記載します。
proxy-wasmlogのクレートを依存関係に追加します。

Cargo.tomlは以下のようになります。

Cargo.toml
[package]
name = "proxy-wasm-plugin-basic-auth"
version = "0.1.0"
edition = "2024"

[dependencies]
proxy-wasm = "0.2"
log = "0.4"

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = true
opt-level = 3
codegen-units = 1
panic = "abort"
strip = "debuginfo"

Google CloudでWasmをアップロードする

gcloud CLI のセットアップを済ませておきます。

ドキュメントにしたがって、RustのWasmをGoogle Cloudにデプロイします。
https://cloud.google.com/service-extensions/docs/prepare-plugin-code#rust

wasmのビルドできることを確認します。

ビルドコマンド
cargo build --release --target wasm32-wasip1

問題なければ、./target/release/wasm32-wasip1/release/proxy_wasm_plugin_basic_auth.wasmが生成されます。

クラウドへのアップロードの準備をします。

Artifact Registry有効化コマンド
# Artifact Registry の有効がまだの時は、有効化しておきます
gcloud services enable artifactregistry.googleapis.com

Dockerでラップしてアップロードします。
まず、アップ先となるArtifact Registryを作成します。
(すでに作成済みの場合はスキップ)

Artifact Registry作成コマンド
# ${GOOGLE_CLOUD_PROJECT}を自分のプロジェクトIDに置き換えてください

gcloud artifacts repositories create service-extensions-wasm-plugin-docker \
    --repository-format=docker \
    --location=asia \
    --project=${GOOGLE_CLOUD_PROJECT} \
    --description="サービス拡張プラグイン" \
    --async

プロジェクトフォルダ直下に、Dockerファイルとcloudbuild.yamlを準備します

Dockerfile
FROM scratch
COPY target/wasm32-wasip1/release/proxy_wasm_plugin_basic_auth.wasm plugin.wasm
cloudbuild.yaml
steps:
- name: 'rust:1-slim-trixie'
  entrypoint: 'bash'
  args:
    - '-c'
    - |
      rustup target add wasm32-wasip1 && \
      cargo build --target=wasm32-wasip1 --release
- name: 'gcr.io/cloud-builders/docker'
  args: [ 'build', '--no-cache', '--platform', 'wasm',
        '-t', 'asia-docker.pkg.dev/$PROJECT_ID/service-extensions-wasm-plugin-docker/proxy_wasm_plugin_basic_auth', '.' ]
images: [ 'asia-docker.pkg.dev/$PROJECT_ID/service-extensions-wasm-plugin-docker/proxy_wasm_plugin_basic_auth' ]

プロジェクトフォルダ一覧は以下のようになります。

.
├── Cargo.lock
├── Cargo.toml
├── cloudbuild.yaml
├── Dockerfile
└── src
    └── lib.rs
└── target
    └── wasm32-wasip1
        └── release
            └── proxy_wasm_plugin_basic_auth.wasm

プロジェクトフォルダのルートで、以下のコマンドを実行して、Wasmをアップロードします。

アップロードコマンド
gcloud builds submit

Google CloudにWasmをデプロイする

ネットワーク サービス APIの有効がまだの時は、有効化しておきます

ネットワーク サービス API有効化コマンド
# ネットワーク サービス APIの有効がまだの時は、有効化しておきます
gcloud services enable networkservices.googleapis.com

Wasmをデプロイ手順は、以下です。

  1. レジストリにアップしたWasmをプラグインとして登録
  2. 登録したプラグインをロードバランサーに適用

ここからは、Google Cloudのコンソール画面で操作します。

プラグイン登録

まず、プラグインを登録します。

ロードバランサー -> Service Extensions -> プラグイン -> プラグインを作成 を選択します。


プラグイン名を適当に設定し、アップロードしたWasmを選択します。

Wasmのアップロードは、以下に上がっています。($PROJECT_IDの部分は自分のプロジェクトIDが入ります)
asia-docker.pkg.dev/$PROJECT_ID/service-extensions-wasm-plugin-docker/proxy_wasm_plugin_basic_auth

プラグインを作成してください。


ロードバランサーへの適用

次に、ロードバランサーにプラグインを適用します。


すべて、「続行」を選択


適当な名前を付けて、適用したいロードバランサーを選択します


  • 「一致条件」はすべての場合に適用したいので、trueにしました
  • 「プログラマビリティのタイプ」をプラグインに設定します
  • 「プラグイン」は先ほどプラグインに登録したものを選択します
  • 「イベント」は、リクエストヘッダーを選択します

数分待つと、プラグインが適用されます。

以上で、Wasmを使ったEdge処理をサーバーにデプロイが完了しました。

感想

Wasmを使ったEdge処理は、Google Cloudの「サービス拡張」を利用することで比較的簡単に実装できることがわかりました。
RustのWasmはパフォーマンスも良く、普通のWebページでストレスなく動作します。

Rust

Rustは、解説記事も多く、学習の助けになりました。
Rustは愛されており、勢いを感じました。

また、エラーもわかりやすく、コンパイルでしっかりチェックしてくれました。
ただ、一部の(Wasmに起因する?)エラーはコンパイルをすり抜けてしました。
そうなったときの(Edge環境での)エラーは解読が難しいと感じます。

WebAssembly(Proxy-Wasm)

今回使ったのは、Proxy-WasmというWebAssemblyの拡張です。
Proxy-Wasmは、オープンな規格のため、他社のサービスでもコードを公開されていることがあります。
そのためプラグイン用のサンプルコードが思ったより豊富で、ちょっとした機能の実装には十分使っていけると感じました。

今回行った、ヘッダー判別の他にもリダイレクト機能や、Meta/X(twitter)に任意のOGP画像を設定する機能なども実装できそうです。

試してみたい方は、こちらのサンプルが参考になります↓
https://github.com/GoogleCloudPlatform/service-extensions/tree/main/plugins#getting-started

注意点として、Proxy-Wasmはタイムアウト時間が厳しいです。
Google Cloudで使うのであれば、タイムリミットが長いCallとうまく使い分けていけば良いと思います。

Google Cloud(サービス拡張)

Google Cloudの「サービス拡張」は、Wasmを使ったEdge処理を簡単に実装できることがわかりました。
サービス拡張を使う条件(パスやヘッダーなど)を設定できるため、特定のリクエストに対してのみ処理を行うことができます。
これと、拡張を動かす条件と(今回は使わなかった)設定情報を渡す機能を使うことでプログラムを変えずに柔軟な可能な場面も多いと思います。

ただ、Edge処理は変更の反映にタイムラグがあり、反映完了がわかりにくいです。
Edgeは反映にラグがあるのは仕方ないのですが、反映完了がわかりやすくなると良いなと思います。

以上です。

Discussion