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

apyluccrwvfa9uxhmu3btdetos4.jpeg

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 с демо-приложением из нескольких микросервисов внутри.

q9a0hi3ydmvlhvhhc1okv9dze1o.png


Создаем 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-<

Примечание. При тестировании мы обнаружили, что фильтр 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-<

Если мы добавили фильтр в цепочку на уровне TCP, вызываются только хуки для TCP-трафика.

Вот наглядная схема того, как это работает с Istio:

gowpjmzcn_cprsiu2gvuxkpfjpo.png


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

© Habrahabr.ru