Пишем фильтры WASM для Envoy и деплоим их с Istio

от автора

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, потому что так нам не придется хранить на локальном компьютере разные библиотеки.

  1. Сначала создаем образ docker с помощью C++ Envoy Proxy WASM SDK, как описано здесь.
  2. Создаем Makefile для фильтра WASM. Makefile:

.PHONY = all clean  PROXY_WASM_CPP_SDK=/sdk  all: example-filter.wasm  include ${PROXY_WASM_CPP_SDK}/Makefile.base_lite

  1. Собираем фильтр 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 ︎

  1. Внедряем код 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"}]'

  1. Выполняем

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

  1. Включаем для фильтров WASM логирование на уровне DEBUG при обработке трафика к сервису frontpage:

kubectl port-forward -n backyards-demo deployment/frontpage-v1 15000  curl -XPOST "localhost:15000/logging?wasm=debug"

  1. Вставляем фильтр 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.

  1. Отправляем трафик через порт 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

  1. Если мы хотим зарегистрировать фильтр 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 — это идеальный способ интегрировать любую логику в сетевое взаимодействие.

ссылка на оригинал статьи https://habr.com/ru/company/southbridge/blog/539484/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *