Search
moon
sun
📌

Proxy-Wasm overview

Category
as S/W 엔지니어
Tags
Istio
Envoy
EnvoyFIlter
WASM
WebAssembly
Rust
Proxy-Wasm
Created time
2025/03/03

Introduction

Istio/Envoy custom plugin 제작을 위한 Proxy-Wasm 검토로서, Proxy-Wasm은 Istio/Envoy와 같은 proxy 환경을 위한 WebAssembly(WASM) runtime 개방형 표준 인터페이스이다. Proxy-Wasm은 WASM을 이끄는 Bytecode Alliance와 Envoy 커뮤니티가 주도한다고.
2025.03.01 현재, 공식 spec repo가 stable latest 버전인 v0.2.1 기준 7개월 전에 commit된 것으로 보아(다음 버전은 2개월 전) 현재까지도 잘 지원이 이루어지는 듯 보인다. 이외에 Istio, Envoy 뿐 아니라 NGNIX를 포함한 유명 proxy 제품이 이를 지원 중(ATS, Kong, APISIX 등).
참고로, 📌golang으로 Proxy-Wasm 빌드&테스트 하기는 Proxy-Wasm 개발의 bootstraping을, 아래는 본 문서의 내용을 기반으로 작성한 Proxy-Wasm Istio/Envoy filter으로, 본 문서에서는 구현 예로 언급한다(Rust 기반).
openapi-endpoint-filter: Request path에 대한 OpenAPI 기반 enpoint를 request header 에 주입하여 Istio 메트릭 레이블의 값으로 사용 가능케 하는 plugin.
openapi-endpoint-filter
anyflow
baggage-filter: 지정한 request header의 key, value를 W3C Trace Context의 baggage header의 항목으로 넣는 plugin.

왜 Lua 대신 WASM?

Lua를 쓰면 EnvoyFilter 등을 통해 WASM 보다 빠르고 간편하게 custom logic을 추가 가능하다(📌Istio EnvoyFilter via mockserver 참고). 하지만 아무리 Lua가 빠르게 동작한다 하더라도 script 기반이므로 proxy처럼 성능에 민감한 영역에는 부담이다. AI 피셜 Rust WASM 기준 2~5배 처리량이 떨어진다 하며 지연이나 리소스 overhead 역시 마찬가지. 기능적으로도 그러하여 대표적으로 Lua로는 metric 처리가 불가능하다. 즉, Lua는 ad-hoc 요구가 주 용도란 뜻. 아래 링크에는 WASM으로 처리 가능한 다수의 Envoy의 영역이 기술되어 있다.

Context

WASM(WebAssembly): 고성능 실행을 위한 브라우저 및 서버용 portable binary 표준으로, 여러 언어 지원, 운영체제 독립적, sandbox 환경이 특징. 참고로, WASI(WebAssembly System Interface)는 WASM이 OS를 호출 가능하게 하는 표준 인터페이스로 파일시스템, 네트워크, 시스템 호출 등을 지원한다. WASM + WASI가 어찌나도 중요해보이는지, docker의 아버지인 Solomon Hykes는 이거 있음 docker 안만들었고, WASM이 미래라고 하며, 실제 container 대체를 위한 많은 움직임이 있다(e.g. k8s용 WASM - runwasi, krustlet 등)
ABI(Application Binary Interface): binary 수준에서의 모듈간 통신을 위한 인터페이스로, 함수 호출, 메모리 정렬, 바이너리 형식 등을 정의. Proxy-Wasm spec은 사실 상 ABI에 대한 spec이다.
Envoy는 비동기 이벤트 기반 아키텍처: 근거는 공식 아키텍처 문서, libevent 사용 코드로서, 현대의 proxy 중 이 모델을 안쓰는 proxy가 있을까 싶다(자체 경량 thread를 쓰는 golang만 예외인 줄 알았더니만 찾아보니 꽤 많다. 경량 thread가 의미있게 뜨는 중인 듯). runtime 아키텍처를 이해하기 위한 context로서, 일정 갯수(e.g. CPU core 갯수)의 worker thread가 만들어진다는 의미이다. 참고로, 공식 문서는 Envoy (란 특정 제품) 를 직접 언급한다.
Envoy Filter: 결국 WASM은 Envoy filter로 쓰기 위해서다. Envoy는 filter chain을 운용하고 filter는 host(Envoy 본체) 및 타 filter와 서로 통신하기 마련이므로 Envoy의 filter chain에 대한 이해가 필요하다. Envoy filter chain 은 이를 상세히 다룬다.

Runtime Architecture

💡
내부 동작 구조, 특히 runtime 하에서 어떻게 동작하는지를 알아야 기능 뿐 아니라 성능을 고려한 설계가 가능하다. 그런 의미에서 아래 그림부터 이해하고 가는게 좋은데, 이 중 worker thread, Wasm runtime, Wasm VM, Proxy-Wasm 간의 관계가 특히 중요하다. 나머지는 그냥 Envoy의 이벤트 기반 아키텍처에 의한 이벤트 발생 시 wasm을 호출하는 (뻔한) 구조를 보여줄 뿐이다.
단일 Thread 내의 Wasm runtime, 그 안의 Wasm VM w/ Proxy-Wasm의 관계가 보인다. 출처: WebAssembly in Envoy
Wasm runtime: Wasm VM의 실행 환경으로 다수의 Wasm VM을 포함한다. Envoy는 V8 기본으로 사용하나 딴 제품도 지원하는 듯(참조). 무엇보다 이 환경에서는 single thread로만 동작하기에 thread 동기화 걱정을 안해도 된다. Wasm VM 간 통신 방법으로는 아래 Feature list에 언급된 shared data, queue 이외에도 header 값을 통한 전달이 있다.
Wasm VM: Wasm bytecode의 실행 인스턴스로서 sandbox 내에서 메모리 관리나 byte code 실행 등을 담당한다. Proxy-Wasm과 1:1 관계에 있다.
중요한 것은 VM을 나누는 단위인데, VM 갯수에 따라 WASM의 startup 지연, resource overhead 및 격리 수준이 달라지기 때문이다. 공식 문서는 이에 대해 5개 모델을 제시하며, Envoy는 wasm module 간 Wasm VM 공유도 가능하도록 한다.
하지만 Istio의 WasmPlugin 은 Envoy의 해당 공유 모델을 지원하지 않으며, 사실 상 module 단위가 VM 단위로 보인다(각기 다른 wasm module에서 plugin_vm_id (링크 참고)를 확인하면 되지만, 알 수 없는 이유로 plugin_name 만 조회 가능하다).
공식 문서의 5개 모델 + Envoy의 추가 모델 상세 내용

오류 대응에 관하여

Proxy 속성 상 가용성이 특히나 중요한데, custom code의 가용성을 Proxy Wasm이 보장할 수는 없다. 이에 대해 안전 장치로, Proxy Wasm 규격은 custom code이 포함된 VM 오류에 대한 동작을 규정한다(링크).
상기 규정에 따라, Istio/Envoy는 plugin에 오류 발생 시 Envoy의 대응 옵션을 제공한다(Istio WasmPlugin 은 적어도 문서 상으로는 Envoy와는 달리 Fail_RELOAD 가 없다. 상기 링크 참조). FAIL_CLOSE 가 default인데 이 경우 proxy가 의도적으로 오동작을 일으키므로 유의해서 사용할 필요가 있다. 문서는 오류 발생 시 단순히 5XX를 반환한다고 하지만, 아래 테스트 결과와 같이 경우에 따라 각기 다른 오동작 결과를 보인다.
FAIL_CLOSE
FAIL_OPEN
image 없음
403 RBAC denied
bypass
on_configure() 중 오류
403 RBAC denied
bypass
on_http_request_headers() 중 오류
request 시 no response
bypass

Proxy-Wasm features

Proxy-Wasm ABI v0.2.1 specification 과 rust SDK source code가 주된 출처로, Proxy-Wasm이 구체적으로 어떤 feature를 제공하는지를 나열한다. 아래 내용은 feature 제공 수준을 감잡기 위한 용도로, 구현 시에는 결국 spec 문서를 보아야 할 것이다. 앞서 잠시 소개한 path-template-filter의 코드를 함께 참고하자.

주요 용어/개념

Plugin: Proxy-Wasm ABI를 구현하는 WebAssembly 모듈
Host: plugin을 운용하는 proxy 자체
Hostcall/Callback: Hostcall은 plugin에서의 host 호출을 의미하고 callback은 그 반대를 의미한다. Rust SDK의 경우 아래 Context의 method 절대 대수가 hostcall/callback의 wrapper이다. 접두어 on_ 이 붙으면 callback, 아니면 hostcall 이다.
# hostcall fn set_http_request_headers(&self, headers: Vec<(&str, &str)>) { hostcalls::set_map(MapType::HttpRequestHeaders, headers).unwrap() } # callback fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { Action::Continue }
Rust
복사
Stream Context 중 하나인 HttpContext의 method 예
Context: Context Root, Stream 둘로 나뉘는데, 구현 관점에서는 그냥 hostcall/callback이 plugin 관점과 특정 stream 또는 request 관점으로 나뉜 것일 뿐이다. Rust에서는 이들을 재정의하여 사용한다.
Root(Plugin) Context: 사실 상 plugin 자체를 의미. plugin 수준에서의 method를 노출한다. configuration 초기화에 주로 사용된다.
Stream Context: 사실 상 특정 request(HTTP)나 stream(TCP)를 의미. request/stream 수준에서의 method를 노출한다. 사실 상 plugin biz logic이 동작하는 위치이다.

Feature list

대강 function 명만으로도 어떤 기능을 담당하는지 짐작 가능하다. 앞선 코드와는 달리 callback method prefix에 proxy_on 이 붙는다. Rust의 경우, 앞서 논했듯 구현 관점에서는 상당 수가 Context로 wrapping되어 제공된다. Context로 제공되지 않아도 hostcall module을 통해 직접 접근 가능하다.
Istio 공식 문서에서의 WASM filter extension 설명. 사실 상 아래와 동일 내용이다. 출처: https://istio.io/latest/docs/concepts/wasm/
Context Lifecycle: proxy_on_context_create, proxy_on_done, proxy_on_log, proxy_on_delete, proxy_done, proxy_set_effective_context
Configuration: proxy_on_vm_start, proxy_on_configure
Logging: proxy_log, proxy_get_log_level
Clock/Timer: proxy_get_current_time_nanoseconds, proxy_set_tick_period_milliseconds, proxy_on_tick
Buffer: proxy_set_buffer_bytes, proxy_get_buffer_bytes, proxy_get_buffer_status
HTTP fields: proxy_get_header_map_size, proxy_get_header_map_pairs, proxy_set_header_map_pairs, proxy_get_header_map_value, proxy_add_header_map_value, proxy_replace_header_map_value, proxy_remove_header_map_value
HTTP/TCP 공통: proxy_continue_stream, proxy_close_stream, proxy_get_status
TCP streams: proxy_on_new_connection, proxy_on_downstream_data, proxy_on_downstream_connection_close, proxy_on_upstream_data, proxy_on_upstream_connection_close
HTTP streams: proxy_on_request_headers, proxy_on_request_body, proxy_on_request_trailers, proxy_on_response_headers, proxy_on_response_body, proxy_on_response_trailers, proxy_send_local_response
HTTP calls: proxy_http_call, proxy_on_http_call_response
gRPC calls: proxy_grpc_call, proxy_grpc_stream, proxy_grpc_send, proxy_grpc_cancel, proxy_grpc_close, proxy_on_grpc_receive_initial_metadata, proxy_on_grpc_receive, proxy_on_grpc_receive_trailing_metadata, proxy_on_grpc_close
공유 key-value store: proxy_set_shared_data, proxy_get_shared_data
공유 queue: proxy_register_shared_queue, proxy_resolve_shared_queue, proxy_enqueue_shared_queue, proxy_dequeue_shared_queue, proxy_on_queue_ready
Metrics: proxy_define_metric, proxy_record_metric, proxy_increment_metric, proxy_get_metric,
Properties: proxy_get_property, proxy_set_property
Foreign function interface(FFI): proxy_call_foreign_function, proxy_on_foreign_function

Envoy filter chain 상 Proxy-Wasm 적용 위치

아래는 일반적인 Istio/Envoy의 filter chain 구성으로, 인입된 request는 아래 순서대로 filter가 적용된다. 적용 순서는 선후 관계에 따라 최종 결과 달라질 수 있기 때문에 중요하다. wasm은 적용 위치 설정에 따라 이들 filter 중 하나를 중심으로 앞 또는 뒤로 적용할 수 있는데, EnvoyFilter 와는 달리 link iconIstioWasm Pluginphase 는 적용 위치가 몇몇 개로 한정된다.
💡
link iconIstioWasm Plugin 의 설명대로라면 phaseUNSPECIFIED_PHASE 로 설정할 경우 8번 router 앞에 붙어야 하지만, 테스트 결과 항상 3번 grpc_stats 앞에 붙는다. 이 위치는 AUTHN 의 경우도 마찬가지이다. 이상하다.
1.
(listener) envoy.filters.listener.tls_inspector: SNI 및 ALPN(Application-Layer Protocol Negotiation; TLS handshaking 시 client/server 간의 프로토콜 협상 메커니즘) 결과를 식별하여, filter chain을 선택
2.
(HTTP) istio.metadata_exchange
3.
(HTTP) envoy.filters.http.grpc_stats
4.
(HTTP) istio.alpn
5.
(HTTP) envoy.filters.http.fault
6.
(HTTP) envoy.filters.http.cors
7.
(HTTP) istio.stats
8.
(HTTP) envoy.filters.http.router