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