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 수정 요청의 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
}
...
이후, 위의 Golang 코드를 TinyGo로 컴파일하여 .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
에서 우리는 HTTP_REQUEST_HEADERS
와 같은 다양한 단계에 따라 cb_name
을 설정하고, ngx_wasm_vm->call
을 통해 VM의 메서드를 호출합니다. 이는 이 글에서 앞서 언급한 wasm 플러그인의 OnHttpRequestHeaders
메서드입니다.
이로써 APISIX가 wasm 플러그인을 호출하고 Golang을 실행하는 전체 호출 체인이 완성됩니다. 호출 체인은 다음과 같습니다:
Wasm VM
Wasm VM은 Wasm 코드를 실행하기 위한 가상 머신입니다. wasm-nginx-module
은 Wasm 코드를 실행하기 위해 "wasmtime"과 "wasmedge" 두 가지 유형의 가상 머신을 구현했습니다. APISIX에서는 Wasm 코드를 실행하기 위해 기본적으로 "wasmtime"을 사용합니다.
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는 Wasm을 활용하여 API 게이트웨이 측에서 더 고급 기능을 제공할 수 있으며, 더 넓은 범위의 사용 시나리오를 해결할 수 있습니다.