サーバーサイドエンジニアの富田です。

静的なWebページを構築した際に、簡単な認証やヘッダー処理を追加したいときありますよね。

AWSではCloud FunctionsやLambda@Edgeがありますが、Google Cloudではなんらかのプロキシサーバーを設置する必要がありました。

しかし最近、Google Cloudで「サービス拡張」がリリースされたことで、ロードバランサーで”ちょっとしたプログラム”を実行できるようになりました。

今回作る機能

「サービス拡張」の中のplugin機能と、Rustを使って簡易なBasic認証の実装します。

動作は、Authorizationヘッダーをチェックする認証を行います。ヘッダーが存在しない、または不正な場合は、401 Unauthorizedレスポンスを返します。

Google Cloudのセットアップなどは終わり、Webページを公開しているところ作業を始めます。

記事のお品書き

1.サービス拡張についてドキュメントで確認する

2.Rustでbasic認証の機能を作る

3.RustをビルドしDockerイメージに固めてクラウドにアップロード

4.アップロードされたDockerイメージをサービス拡張のpluginとして登録

5. 登録されたpluginをロードバランサー適用🚀デプロイ環境

それでは、さっそく始めましょう。

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

「サービス拡張」のドキュメントを見ながら進めます。

https://cloud.google.com/service-extensions/docs/plugins-overview

サービスを作る

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

ドキュメント

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

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

// rust: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

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

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

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

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

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-wasm`と`log`のクレートを依存関係に追加します。

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 の有効がまだの時は、コマンドを実行して有効化しておきます

gcloud services enable artifactregistry.googleapis.com

Dockerでラップしてアップロードします。

まず、アップ先となる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の有効がまだの時は、有効化しておきます

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処理をサーバーにデプロイが完了しました。

感想

Rust+Proxy-Wasm+サービス拡張について

Rustは今回初めて使ったのですが、解説記事も多く言語の勢いを感じました。

エラーもわかりやすく、コンパイルでしっかりチェックしてくれるので実装がしやすかったです。

ただ、一部の(Wasmに起因する?)エラーがコンパイルをすり抜けてしまったことがあり、Edge処理の環境下では修正に時間がかかったので注意が必要そうです。

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

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

試してみたい方は、こちらのサンプルが参考になります↓

https://github.com/GoogleCloudPlatform/service-extensions/tree/main/plugins#getting-started

注意点として、Proxy-Wasmはタイムアウト時間が厳しいです。

Google Cloudで使うのであれば、タイムリミットが長いCallとうまく使い分けていけば良いと思います。

最後に、Google Cloudの「サービス拡張」は、Wasmを使ったEdge処理を簡単に実装できることがわかりました。

サービス拡張を使う条件(パスやヘッダーなど)を設定できるため、特定のリクエストに対してのみ処理を行うことができます。

これと、拡張を動かす条件と(今回は使わなかった)設定情報を渡す機能を使うことでプログラムを変えずに柔軟な可能な場面も多いと思います。

ただ、Edge処理は変更の反映にタイムラグがあり、反映完了がわかりにくいので注意が必要です。 もう少し反映完了をわかりやすくしてほしいところです。

総括

サービス拡張は便利な機能です。 サービス拡張自体の記事はまだ少ないですが、開発するのに必要な「Rust」や「Proxy-Wasm」の技術はオープンなものですので調べて使っていくことができます。

気になる方は触ってみはいかがでしょうか。



■ワントゥーテンでは中途採用募集中です!

1→10(ワントゥーテン)のカルチャーや、作品のクリエイティブに共感し、自身のより高い成長を求めている方からのご応募をお待ちしています!