Apache APISIXがWasm機能を実現する方法

Xinxin Zhu

April 7, 2023

Products

高性能なアプリケーションをウェブブラウザで実行するために、Apache APISIXはゲートウェイレベルでWasmをサポートしています。Wasmは、ウェブ上でコードを効率的かつ安全に実行するためのバイナリ命令形式です。これにより、開発者はC/C++/Go/Rustなどの高級プログラミング言語を使用し、proxy-wasm仕様に従ってWasmプラグインを作成できます。proxy-wasm仕様は、他のシステムとの互換性と相互運用性を保証します。この記事を通じて、詳細を学びましょう。

Wasmとは

WebAssembly(略称Wasm)は、スタックベースの仮想マシン向けのバイナリ命令形式です。

Wasmが登場する以前は、ウェブブラウザで実行できるのはJavascriptだけでした。しかし、Wasmの導入により、C/C++/Golangなどの高級言語もウェブブラウザで実行できるようになりました。Chrome、Firefox、Safariなどの主要なブラウザはすべてWasmをサポートしています。さらに、WASI(WebAssembly System Interface)プロジェクトの進展により、サーバーサイド環境でもWasm命令の実行が可能になりました。

現在、Apache APISIXはゲートウェイレベルでWasmをサポートしています。開発者はC/C++/Go/Rustなどの高級プログラミング言語を使用し、proxy-wasm仕様に従ってWasmプラグインを作成できます。

wasm

APISIXがWasmプラグインをサポートする理由

ネイティブのLuaプラグインと比較して、Wasmプラグインには以下の利点があります:

  • 拡張性: Wasmをサポートすることで、APISIXはproxy-wasmが提供するSDKを使用して、C++、Golang、Rustなどの高級プログラミング言語でプラグインを開発できます。これらの言語はより豊かなエコシステムを持っているため、開発者はより多くの機能を備えたプラグインを実装できます。

  • セキュリティ: APISIXとWasmの相互作用はproxy-wasmが提供するApplication Binary Interface(ABI)に依存しているため、このアクセスはより安全です。Wasmプラグインはリクエストに対して特定の変更しか行えないため、悪意のある動作を防ぐことができます。さらに、Wasmプラグインは別のVMで実行されるため、プラグインがクラッシュしてもAPISIXのメインプロセスに影響を与えません。

APISIXがWasmをサポートする仕組み

Wasmが何かを理解したところで、APISIXがどのようにWasmプラグインをサポートするかをトップダウンで見ていきましょう。

apisix-wasm

APISIX Wasmプラグイン

APISIXは、開発者がC/C++、Go、Rustなどの人気のある高級プログラミング言語を使用してプラグインを作成できるようにしています。これらのプラグインは、対応するSDKを使用し、proxy-wasm仕様に従って構築できます。

proxy-wasmは、Envoyによって導入されたL4/L7プロキシ間のABI仕様です。この仕様は、メモリ管理、L4プロキシ、L7プロキシ拡張を含むABIを定義しています。例えば、HTTP(L7)では、proxy-wasm仕様はproxy_on_http_request_headersproxy_on_http_request_bodyproxy_on_http_request_trailersproxy_on_http_response_headersなどのABIを定義し、モジュールがさまざまな段階でリクエスト内容を取得および変更できるようにします。

例えば、Golangとproxy-wasm-go-sdkを使用してこのプラグインを開発します:

proxy-wasm-go-sdkは、proxy-wasm仕様のSDKで、開発者がGolangを使用してproxy-wasmプラグインをより簡単に作成できるようにします。ただし、ネイティブGolangのWASIサポートにいくつかの問題があるため、このSDKはTinyGoに基づいて実装されています。詳細については、こちらを参照してください。

このプラグインの主な機能は、HTTPリクエストのレスポンスステータスコードとレスポンスボディを変更することです。これはAPISIXリンクから参照されています。

...
func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
        data, err := proxywasm.GetPluginConfiguration()
        if err != nil {
                proxywasm.LogErrorf("error reading plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }

        var p fastjson.Parser
        v, err := p.ParseBytes(data)
        if err != nil {
                proxywasm.LogErrorf("error decoding plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }
        ctx.Body = v.GetStringBytes("body")
        ctx.HttpStatus = uint32(v.GetUint("http_status"))
        if v.Exists("percentage") {
                ctx.Percentage = v.GetInt("percentage")
        } else {
                ctx.Percentage = 100
        }

        // schema check
        if ctx.HttpStatus < 200 {
                proxywasm.LogError("bad http_status")
                return types.OnPluginStartStatusFailed
        }
        if ctx.Percentage < 0 || ctx.Percentage > 100 {
                proxywasm.LogError("bad percentage")
                return types.OnPluginStartStatusFailed
        }

        return types.OnPluginStartStatusOK
}

func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
        plugin := ctx.parent
        if !sampleHit(plugin.Percentage) {
                return types.ActionContinue
        }

        err := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1)
        if err != nil {
                proxywasm.LogErrorf("failed to send local response: %v", err)
                return types.ActionContinue
        }
        return types.ActionPause
}
...

その後、TinyGoを使用して上記のGolangコードをコンパイルし、.wasmファイルを生成します。

tinygo build -o wasm_fault_injection.go.wasm -scheduler=none -target=wasi ./main.go

コンパイルが完了すると、fault_injection.go.wasmファイルが得られます。

wasmファイルの内容に興味がある場合は、wasm-toolを使用してwasmファイルの具体的な内容を確認できます。 wasm-tools dump hello.go.wasm

APISIXのconfig.yamlファイルにwasm_fault_injection.go.wasmを設定し、プラグイン名をwasm_fault_injectionとします。

apisix:
        ...
wasm:
  plugins:
    - name: wasm_fault_injection
      priority: 7997
      file: wasm_fault_injection.go.wasm

その後、APISIXを起動し、Wasmプラグインを参照するルートを作成します:

curl  http://127.0.0.1:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{
    "uri":"/*",
    "upstream":{
        "type":"roundrobin",
        "timeout":{
            "connect":1,
            "read":1,
            "send":1
        },
        "nodes":{
            "httpbin.org:80":1
        }
    },
    "plugins":{
        "wasm_fault_injection":{
            "conf":"{\"http_status\":200, \"body\":\"Hello WebAssembly!\n\"}"
        }
    },
    "name":"wasm_fault_injection"
}'

アクセステストを行った後、レスポンスボディが「Hello WebAssembly」に変更されていることが確認でき、Wasmプラグインが有効になっていることがわかります。

curl 127.0.0.1:9080/get -v
*   Trying 127.0.0.1:9080...
* Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
> GET /get HTTP/1.1
> Host: 127.0.0.1:9080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 09 Feb 2023 07:46:50 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: APISIX/3.1.0
<
Hello WebAssembly!

Wasm-nginx-module

Apache APISIXがWasmプラグインを使用する方法を学んだ後、次に「なぜWasmプラグインでリクエスト内容にアクセスして変更できるのか?」について深く掘り下げます。

APISIXはOpenRestyを基盤とするフレームワークを使用しているため、Wasmプラグインでリクエスト内容にアクセスして変更するには、OpenRestyまたはNGINXが提供するAPIとやり取りする必要があります。これがwasm-nginx-moduleの役割です。

wasm-nginx-moduleは、WasmをサポートするNGINXモジュールで、API7によって開発されました。このモジュールは、NGINXに基づいてproxy-wasm-abiを実装し、Lua APIをカプセル化して、Luaレベルでproxy-wasm-abi呼び出しを完了できるようにします。詳細については、wasm-nginx-moduleを参照してください。

例えば、APISIXがaccessフェーズに到達すると、wasm-nginx-moduleが提供するLuaメソッドon_http_request_headersを呼び出します。

-- apisix/wasm.lua
...
local ok, err = wasm.on_http_request_headers(plugin_ctx)
   if not ok then
       core.log.error(name, ": failed to run wasm plugin: ", err)
       return 503
   end
end
...

その後、このメソッド内でwasm-nginx-moduleが提供するngx_http_wasm_on_httpメソッドが呼び出されます。

ngx_int_t
ngx_http_wasm_on_http(ngx_http_wasm_plugin_ctx_t *hwp_ctx, ngx_http_request_t *r,
                      ngx_http_wasm_phase_t type, const u_char *body, size_t size,
                      int end_of_body)
{
    ...

    ctx = ngx_http_wasm_get_module_ctx(r);

    if (type == HTTP_REQUEST_HEADERS) {
        cb_name = &proxy_on_request_headers;
    } else if (type == HTTP_REQUEST_BODY) {
        cb_name = &proxy_on_request_body;
    } else if (type == HTTP_RESPONSE_HEADERS) {
        cb_name = &proxy_on_response_headers;
    } else {
        cb_name = &proxy_on_response_body;
    }

    if (type == HTTP_REQUEST_HEADERS || type == HTTP_RESPONSE_HEADERS) {
        if (hwp_ctx->hw_plugin->abi_version == PROXY_WASM_ABI_VER_010) {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32, http_ctx->id, 0);
        } else {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                                   0, 1);
        }

    } else {
        rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                              cb_name,
                              true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                              size, end_of_body);
    }
    ...
}

wasm-nginx-moduleでは、異なる段階に基づいてcb_nameを設定します。例えば、HTTP_REQUEST_HEADERSproxy_on_request_headersに対応し、その後ngx_wasm_vm->callを通じてVM内のメソッドを呼び出します。これは、この記事の前半で説明したwasmプラグインのOnHttpRequestHeadersメソッドです。

これにより、APISIXがwasmプラグインを呼び出し、Golangを実行するための呼び出しチェーンが完成します。呼び出しチェーンは以下の通りです:

Wasm VM

Wasm VMは、Wasmコードを実行するための仮想マシンです。wasm-nginx-moduleは、Wasmコードを実行するために「wasmtime」と「wasmedge」の2種類の仮想マシンを実装しています。APISIXでは、デフォルトで「wasmtime」がWasmコードを実行します。

Wasmtimeは、Bytecode Allianceによってオープンソース化された、WebAssemblyとWASIのための高速で安全なランタイムです。ウェブ環境外でWebAssemblyコードを実行でき、コマンドラインツールとして使用したり、他のプログラムにWebAssemblyランタイムエンジンとしてライブラリとして組み込むことができます。Wasmedgeは、エッジコンピューティング向けに最適化された軽量で高性能なWebAssembly(Wasm)仮想マシンです。クラウドネイティブ、エッジ、分散アプリケーションに使用できます。

まず、Wasm VMでは、loadメソッドを使用して.wasmファイルをメモリにロードします。その後、VMのcallメソッドを使用してこれらの関数を呼び出すことができます。VMはWASIインターフェースの実装に基づいており、これによりWasmコードはブラウザ側だけでなくサーバー側でも実行できるようになります。

まとめ

Wasmが何であるか、そしてAPISIXがどのようにWasmプラグインをサポートするかを理解しました。Wasmプラグインのサポートを提供することで、APISIXはC++、Rust、Golang、AssemblyScriptなどの複数の言語でのプラグイン開発をサポートする能力を強化するだけでなく、WebAssemblyの広範なエコシステムとユースケースの恩恵を受けることができます。これにより、APISIXはAPIゲートウェイ側でより高度な機能を提供し、より広範な使用シナリオに対応できるようになります。

Tags: