Apache APISIXがWasm機能を実現する方法
Xinxin Zhu
April 7, 2023
高性能なアプリケーションをウェブブラウザで実行するために、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プラグインを作成できます。
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は、開発者がC/C++、Go、Rustなどの人気のある高級プログラミング言語を使用してプラグインを作成できるようにしています。これらのプラグインは、対応するSDKを使用し、proxy-wasm仕様に従って構築できます。
proxy-wasmは、Envoyによって導入されたL4/L7プロキシ間のABI仕様です。この仕様は、メモリ管理、L4プロキシ、L7プロキシ拡張を含むABIを定義しています。例えば、HTTP(L7)では、proxy-wasm仕様は
proxy_on_http_request_headers
、proxy_on_http_request_body
、proxy_on_http_request_trailers
、proxy_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_HEADERS
はproxy_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ゲートウェイ側でより高度な機能を提供し、より広範な使用シナリオに対応できるようになります。