Envoy — это высокопроизводительный программируемый прокси L3/L4 и L7, на котором основано множество реализаций service mesh, например, Istio. Envoy обрабатывает трафик с помощью сетевых фильтров, которые можно объединять в цепочки, чтобы реализовывать сложные функции для контроля доступа, преобразования, обогащения данных, аудита и так далее. Чтобы расширить функционал Envoy, новые фильтры можно добавить одним из двух способов:
- Интегрируем дополнительные фильтры в исходный код Envoy и компилируем новую версию Envoy. Недостаток такого подхода в том, что придется поддерживать свою версию Envoy и постоянно синхронизировать ее с официальным дистрибутивом. Фильтр, кстати, нужно реализовать на C++, как и сам Envoy.
- Динамически загружаем новые фильтры в Envoy Proxy в рантайме.
Второй вариант гораздо интереснее и проще — мы используем WebAssembly (WASM), эффективный и портативный бинарный формат инструкций со встраиваемой и изолированной средой выполнения.
Расскажу подробнее о фильтрах WASM.
Почему фильтры WASM? ?
Плюсы фильтров WASM:
- Гибкость — фильтры можно динамически загружать в запущенный процесс Envoy без остановки или перекомпиляции.
- Простота использования — мы расширяем функционал Envoy, не меняя кодовую базу.
- Разнообразие — мы можем выбрать язык для реализации фильтров, например C/C++, Rust или golang, и скомпилировать его в WASM.
- Надежность и изоляция — мы деплоим фильтры на виртуальной машине (в песочнице) изолированно от самого процесса Envoy (если что-то пойдет не так, процесс не пострадает).
- Безопасность — фильтры общаются с хостом (Envoy Proxy) через продуманный API, поэтому у них есть доступ к ограниченному числу соединений или свойств запросов.
Минусы, конечно, тоже есть:
- Производительность на уровне 70% от C++.
- Нужно больше памяти, чтобы запускать виртуальные машины для WASM.
Envoy Proxy WASM SDK ?
Envoy Proxy выполняет фильтры WASM внутри виртуальной машины на основе стека, поэтому память фильтра изолирована от хост-среды. Все взаимодействия между хостом (Envoy Proxy) и фильтром WASM реализуются через функции и обратные вызовы, предоставляемые Envoy Proxy WASM SDK. С Envoy Proxy WASM SDK можно выбрать разные языки:
Здесь я расскажу, как писать фильтры WASM для Envoy с помощью C++ Envoy Proxy WASM SDK. Мы не будем подробно останавливаться на API для Envoy Proxy WASM SDK, но постараемся разобраться в основах написания фильтров WASM для Envoy.
Для реализации фильтров нам нужны два класса:
class RootContext;
class Context;
Когда мы загружаем плагин WASM (бинарный код WASM с фильтром), создается root context. Root context существует столько же, сколько инстанс виртуальной машины, который выполняет фильтр. Его задачи:
- взаимодействия между кодом и Envoy Proxy при начальной настройке;
- взаимодействия, которые продолжат существовать после запроса.
onConfigure(size_t)
вызывается Envoy Proxy в RootContext
только для передачи конфигураций в виртуальную машину и плагин. Если плагин с одним или несколькими фильтрами ожидает от Envoy Proxy конфигурацию, эту функцию можно отменить и получить конфигурацию с помощью вспомогательной функции getBufferBytes
через WasmBufferType::VmConfiguration
и WasmBufferType::PluginConfiguration
соответственно.
Сетевой трафик, обрабатываемый Envoy Proxy, будет проходить через цепочку фильтров, связанную с listener, который получает этот трафик. Для каждого нового потока через цепочку фильтров Envoy Proxy создает новый контекст, который существует до конца потока.
Базовый класс Context
предоставляет хуки (обратные вызовы) в виде виртуальных функций onXXXX(...)
для трафика HTTP и TCP, которые вызываются, когда Envoy Proxy проходит по цепочке фильтров. Обратные вызовы в Context
зависят от уровня цепочки фильтров, в которую входит фильтр (HTTP или TCP). Например, FilterHeadersStatus onRequestHeaders(uint32_t)
вызывается только для фильтров WASM в цепочке на уровне HTTP, но не для TCP.
Реализация базового класса Context
используется Envoy Proxy для взаимодействия с кодом на протяжении времени существования потока. В этих функциях обратных вызовов мы можем управлять трафиком. SDK предоставляет функции для управления заголовками HTTP-запросов и ответов (getRequestHeader
, addRequestHeader
и т. д.), телом HTTP-запроса, TCP-потоками (например, getBufferBytes
, setBufferBytes
) и т. д. Каждая функция обратного вызова возвращает статус, по которому Envoy Proxy узнает, надо или нет передавать обработку потока на следующий фильтр в цепочке.
Следующий шаг — зарегистрировать инстансы factory
, чтобы создать реализации RootContext
и Context
через объявление статической переменной типа
class RegisterContextFactory;
Переменная будет ждать root context factory и context factory в виде аргументов конструктора.
Пример фильтра ?
Вот очень простой пример скелета фильтра WASM, который можно создать с C++ Envoy Proxy WASM SDK: example-filter.cc:
#include "proxy_wasm_intrinsics.h"
class ExampleRootContext: public RootContext {
public:
explicit ExampleRootContext(uint32_t id, StringView root_id): RootContext(id, root_id) {}
bool onStart(size_t) override;
};
class ExampleContext: public Context {
public:
explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {}
FilterHeadersStatus onResponseHeaders(uint32_t) override;
FilterStatus onDownstreamData(size_t, bool) override;
};
// register factories for ExampleContext and ExampleRootContext
static RegisterContextFactory register_FilterContext(CONTEXT_FACTORY(ExampleContext),
ROOT_FACTORY(ExampleRootContext),
"my_root_id");
// invoked when the plugin initialised and is ready to process streams
bool ExampleRootContext::onStart(size_t n) {
LOG_DEBUG("ready to process streams");
return true;
}
// invoked when HTTP response header is decoded
FilterHeadersStatus ExampleContext::onResponseHeaders(uint32_t) {
addResponseHeader("resp-header-demo", "added by our filter");
return FilterHeadersStatus::Continue;
}
// invoked when downstream TCP data chunk is received
FilterStatus ExampleContext::onDownstreamData(size_t, bool) {
auto res = setBuffer(WasmBufferType::NetworkDownstreamData, 0, 0, "prepend payload to downstream data");
if (res != WasmResult::Ok) {
LOG_ERROR("Modifying downstream data failed: " + toString(res));
return FilterStatus::StopIteration;
}
return FilterStatus::Continue;
}
Сборка фильтра
Самый простой способ собрать фильтр — использовать Docker, потому что так нам не придется хранить на локальном компьютере разные библиотеки.
- Сначала создаем образ docker с помощью C++ Envoy Proxy WASM SDK, как описано здесь.
- Создаем Makefile для фильтра WASM. Makefile:
.PHONY = all clean
PROXY_WASM_CPP_SDK=/sdk
all: example-filter.wasm
include ${PROXY_WASM_CPP_SDK}/Makefile.base_lite
- Собираем фильтр WASM:
docker run -v $PWD:/work -w /work wasmsdk:v2 /build_wasm.sh
Деплоим фильтр WASM с Istio ?
Узнать о работе с Istio и внедрении service mesh можно на интенсиве 19—21 марта.
Деплоим наш фильтр Envoy WASM для приложения, запущенного в Istio service mesh в Kubernetes. Можем быстро запустить Istio mesh с демо-приложением в Kubernetes с помощью Backyards, дистрибутива Istio от Banzai Cloud. (прим. переводчика: также можно воспользоваться этой getting started инструкцией до шага Deploy the sample application включительно и далее использовать bookinfo приложение в следующих шагах).
backyards install -a --run-demo
Всего одна команда — и к нашим услугам production-ready и полностью рабочая Istio service mesh с демо-приложением из нескольких микросервисов внутри.
Создаем config map для кода wasm
Создаем config map, где будет размещаться код WASM
для нашего фильтра, в неймспейс backyards-demo
, где запущено демо (прим. переводчика:либо bookinfo в случае использования чистого Istio).
kubectl create cm -n backyards-demo example-filter --from-file=example-filter.wasm
Внедряем код wasm в демо с помощью Istio ?
- Внедряем код wasm в сервис
frontpage
нашего демо-приложения с помощью двух аннотаций:
sidecar.istio.io/userVolume: '[{"name":"wasmfilters-dir","configMap": {"name": "example-filter"}}]'
sidecar.istio.io/userVolumeMount: '[{"mountPath":"/var/local/lib/wasm-filters","name":"wasmfilters-dir"}]'
- Выполняем
kubectl scale deployment -n backyards-demo frontpage-v1 --replicas=1
kubectl patch deployment -n backyards-demo frontpage-v1 -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/userVolume":"[{\"name\":\"wasmfilters-dir\",\"configMap\": {\"name\": \"example-filter\"}}]","sidecar.istio.io/userVolumeMount":"[{\"mountPath\":\"/var/local/lib/wasm-filters\",\"name\":\"wasmfilters-dir\"}]"}}}}}'
Теперь код фильтра WASM доступен в /var/local/lib/wasm-filters
в контейнере istio-proxy:
kubectl exec -n backyards-demo -it deployment/frontpage-v1 -c istio-proxy -- ls /var/local/lib/wasm-filters/
example-filter.wasm
- Включаем для фильтров WASM логирование на уровне DEBUG при обработке трафика к сервису
frontpage
:
kubectl port-forward -n backyards-demo deployment/frontpage-v1 15000
curl -XPOST "localhost:15000/logging?wasm=debug"
- Вставляем фильтр WASM в цепочку на уровне HTTP, привязанную к порту HTTP 8080:
kubectl apply -f-<<EOF
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: frontpage-v1-examplefilter
namespace: backyards-demo
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
proxy:
proxyVersion: '^1\.5.*'
listener:
portNumber: 8080
filterChain:
filter:
name: envoy.http_connection_manager
subFilter:
name: envoy.router
patch:
operation: INSERT_BEFORE
value:
config:
config:
name: example-filter
rootId: my_root_id
vmConfig:
code:
local:
filename: /var/local/lib/wasm-filters/example-filter.wasm
runtime: envoy.wasm.runtime.v8
vmId: example-filter
allow_precompiled: true
name: envoy.filters.http.wasm
workloadSelector:
labels:
app: frontpage
version: v1
EOF
Примечание. При тестировании мы обнаружили, что фильтр portNumber
, указанный для listener match в кастомном ресурсе EnvoyFilter
, некорректно обрабатывался в Istio, поэтому хуки для фильтра не вызывались. Мы исправили эту проблему в нашем дистрибутиве Istio — Backyards.
- Отправляем трафик через порт HTTP 8080 в сервис
frontpage
:
kubectl run curl --image=yauritux/busybox-curl --restart=Never -it --rm sh
/home # curl -L -v http://frontpage.backyards-demo:8080
* About to connect() to frontpage.backyards-demo port 8080 (#0)
* Trying 10.10.178.38...
* Adding handle: conn: 0x10eadbd8
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x10eadbd8) send_pipe: 1, recv_pipe: 0
* Connected to frontpage.backyards-demo (10.10.178.38) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.30.0
> Host: frontpage.backyards-demo:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Thu, 16 Apr 2020 16:32:20 GMT
< content-length: 9
< x-envoy-upstream-service-time: 10
< resp-header-demo: added by our filter
< x-envoy-peer-metadata: CjYKDElOU1RBTkNFX0lQUxImGiQxMC4yMC4xLjU3LGZlODA6OmQwNDM6NDdmZjpmZWYwOmVkMjkK2QEKBkxBQkVMUxLOASrLAQoSCgNhcHASCxoJZnJvbnRwYWdlCiEKEXBvZC10ZW1wbGF0ZS1oYXNoEgwaCjU3OGM2NTU0ZDQKJAoZc2VjdXJpdHkuaXN0aW8uaW8vdGxzTW9k
ZRIHGgVpc3RpbwouCh9zZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1uYW1lEgsaCWZyb250cGFnZQorCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2lvbhIEGgJ2MQoPCgd2ZXJzaW9uEgQaAnYxChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAonCgROQU1FEh8aHWZyb250cGFnZS12MS01N
zhjNjU1NGQ0LWxidnFrCh0KCU5BTUVTUEFDRRIQGg5iYWNreWFyZHMtZGVtbwpXCgVPV05FUhJOGkxrdWJlcm5ldGVzOi8vYXBpcy9hcHBzL3YxL25hbWVzcGFjZXMvYmFja3lhcmRzLWRlbW8vZGVwbG95bWVudHMvZnJvbnRwYWdlLXYxCi8KEVBMQVRGT1JNX01FVEFEQVRBEhoqGAoWCgpjbHVzdGVyX2lkEg
gaBm1hc3RlcgocCg9TRVJWSUNFX0FDQ09VTlQSCRoHZGVmYXVsdAofCg1XT1JLTE9BRF9OQU1FEg4aDGZyb250cGFnZS12MQ==
< x-envoy-peer-metadata-id: sidecar~10.20.1.57~frontpage-v1-578c6554d4-lbvqk.backyards-demo~backyards-demo.svc.cluster.local
< x-by-metadata: CjYKDElOU1RBTkNFX0lQUxImGiQxMC4yMC4xLjU3LGZlODA6OmQwNDM6NDdmZjpmZWYwOmVkMjkK2QEKBkxBQkVMUxLOASrLAQoSCgNhcHASCxoJZnJvbnRwYWdlCiEKEXBvZC10ZW1wbGF0ZS1oYXNoEgwaCjU3OGM2NTU0ZDQKJAoZc2VjdXJpdHkuaXN0aW8uaW8vdGxzTW9kZRIHGgVp
c3RpbwouCh9zZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1uYW1lEgsaCWZyb250cGFnZQorCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2lvbhIEGgJ2MQoPCgd2ZXJzaW9uEgQaAnYxChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAonCgROQU1FEh8aHWZyb250cGFnZS12MS01NzhjNjU1N
GQ0LWxidnFrCh0KCU5BTUVTUEFDRRIQGg5iYWNreWFyZHMtZGVtbwpXCgVPV05FUhJOGkxrdWJlcm5ldGVzOi8vYXBpcy9hcHBzL3YxL25hbWVzcGFjZXMvYmFja3lhcmRzLWRlbW8vZGVwbG95bWVudHMvZnJvbnRwYWdlLXYxCi8KEVBMQVRGT1JNX01FVEFEQVRBEhoqGAoWCgpjbHVzdGVyX2lkEggaBm1hc3
RlcgocCg9TRVJWSUNFX0FDQ09VTlQSCRoHZGVmYXVsdAofCg1XT1JLTE9BRF9OQU1FEg4aDGZyb250cGFnZS12MQ==
* Server istio-envoy is not blacklisted
< server: istio-envoy
< x-envoy-decorator-operation: frontpage.backyards-demo.svc.cluster.local:8080/*
<
* Connection #0 to host frontpage.backyards-demo left intact
frontpage
- Если мы хотим зарегистрировать фильтр WASM в цепочке TCP для сервиса
frontpage
, который принимает TCP на порте 8083, кастомный ресурсEnvoyFilter
будет выглядеть как-то так:
kubectl apply -f-<<EOF
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: frontpage-v1-examplefilter
namespace: backyards-demo
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: SIDECAR_INBOUND
proxy:
proxyVersion: '^1\.5.*'
listener:
portNumber: 8083
filterChain:
filter:
name: "envoy.tcp_proxy"
patch:
operation: INSERT_BEFORE
value:
config:
config:
name: example-filter
rootId: my_root_id
vmConfig:
code:
local:
filename: /var/local/lib/wasm-filters/example-filter.wasm
runtime: envoy.wasm.runtime.v8
vmId: example-filter
allow_precompiled: true
name: envoy.filters.network.wasm
workloadSelector:
labels:
app: frontpage
version: v1
EOF
Если мы добавили фильтр в цепочку на уровне TCP, вызываются только хуки для TCP-трафика.
Вот наглядная схема того, как это работает с Istio:
Пишем фильтры WASM для Envoy с WASME ?
Solo.io предложили решение для разработки фильтров WASM для Envoy — WebAssembly Hub, чтобы загружать и выгружать свои коды фильтров WASM. Используйте инструмент WASME для скаффолдинга, сборки и отправки фильтров WASM в WebAssembly Hub.
При деплое фильтра WASM wasme
вытаскивает образ с плагином фильтра WASM из WebAssembly Hub, запускает daemonset, чтобы извлечь код плагина WASM из этого образа, и открывает его для Envoy Proxy на каждой ноде через тома hostPath
.
Примечание. Образы из WebAssembly Hub не будут отображаться как стандартные образы Docker.
Правда, тут мы публикуем и храним фильтры WASM в стороннем хранилище (WebAssembly Hub), так что этот вариант вам не подойдет, если из-за строгих политик безопасности или по другой причине вы не хотите обнародовать проприетарный код, даже в бинарном формате, за пределами корпоративной сети.
Заключение ?
С фильтрами WASM для Envoy можно написать свой код, скомпилировать его в плагины WASM и настроить Envoy для его выполнения. Плагины могут содержать произвольную логику, поэтому подходят для любых интеграций и изменений в сообщениях. Так что фильтры WASM для Envoy Proxy — это идеальный способ интегрировать любую логику в сетевое взаимодействие.
dmitry_rozhkov
Интересно, а что с ними не так?