Envoy — как писать чистый бизнес код для микросервисной архитектуры
Привет, Хабр, это моя первая статья. Меня зовут Константин, я системный инженер в компании ГНИВЦ. Здесь я хотел бы вам рассказать, что такое Envoy и как с его помощью можно упростить жизнь разработчикам и повысить надёжность взаимодействия микросервисов, минуя инфраструктуру для кого-то страшного и непонятного Kubernetes, а используя простой и старомодный Docker. Также эта статья поможет познакомиться с Envoy поближе и узнать, как он шагает в ногу с таким проектом как Istio.
Что это такое?
Envoy — это L4-L7-балансировщик, написанный на C++ и ориентированный на высокую производительность и доступность. Он обладает отличной observability, в отличие от обычного Nginx, где по метрикам всё скудно. Интересней разве что Nginx+, но сегодня мы рассматриваем open-source решения. Envoy включает множество настроек для проксирования, они же фильтры, и возможностей для обеспечения безопасности.
Что будем рассматривать?
В статье хотелось бы рассказать про различные варианты настройки Envoy с примерами — для тех, кто не хочет читать официальную документацию, которая была автосгенерирована по .proto
файлам, а ознакомиться и понять:, а нужно ли нам это вообще? В основном берём статическую конфигурацию. В этой статье я не буду затрагивать динамическую конфигурацию и протокол xDS — это уже отдельная история, которую я опишу позже. Из технологий которые затронем: observability, circuit breaking, authentication, authorization, outlier detection, healt check, retries, mtls и, конечно же, паттерны отказоустойчивости.
Как envoy может помочь при разработке?
Здесь относительно всё просто. Разработчики, когда пишут REST API, очень много времени могут тратить не на бизнес-код, а на различную логику, которую можно заменить одним только прокси. Так вот вопрос:, а чем же мы, как инженеры, можем им помочь? Вот, например, какие задачи может решить Envoy:
Изоляция инфраструктурных задач — сюда могут входить retries и таймауты на выполнение различных запросов, балансировка, роутинг. Под роутингом мы можем деплоить как A/B-тестирование, canary, blue/green — смысл, я думаю, понятен.
Снятие нагрузки с бизнес-логики — Envoy умеет аутентифицировать и авторизировать запросы, а именно через OAuth2, JWT, RBAC. И с точки зрения безопасности есть что покрутить — SSL/TLS и лимитирование запросов (rate limit).
Мониторинг и трассировка — сюда входят логирование, метрики в формате OpenMetrics для Prometheus, а также поддержка трассировки (Jaeger, Zipkin, OpenTelemetry). Если один сервис вызывает другой, Envoy может автоматически вставлять trace ID для отслеживания потока данных.
Обеспечение надежности — сюда можно отнести Circuit Breakers, Outlier Detection, Health Check
На выходе мы должны получить меньшее количество багов (но это не точно) и большее количество фичей в продакшене.
Паттерны отказоустойчивости
Circuit Breaker (прерывание цепи) — предотвращение перегрузки зависимых сервисов. Envoy поддерживает механизм circuit breaking, который отключает отправку запросов к сервису, если он становится недоступным или превышает заданные лимиты (например, по количеству ошибок, задержкам или запросам).
Retries and Timeouts (повторы и таймауты) — повторение запросов в случае временных сбоев или задержек. Envoy может автоматически повторять запросы в случае ошибок (например, 5xx, 4xx, таймаутов). Вы также можете задать стратегию ограничений на повторы.
Outlier Detection (выявление «проблемных» хостов) — автоматическое исключение хостов из кластера, если они ведут себя нестабильно (например, медленные ответы или высокая частота ошибок).Этот механизм позволяет исключать проблемные узлы из пула доступных конечных точек. Поддерживает проверки: HTTP, gRPC, Redis и Thrift
Load Balancing (балансировка нагрузки) — распределение запросов по доступным узлам.Envoy поддерживает несколько стратегий балансировки нагрузки, а именно Round Robin, Least Request, Random, Maglev, Ring Hash. Кому интересно почитать дальше то вам сюда
Rate Limiting (ограничение скорости запросов) — защита от перегрузки путем ограничения частоты запросов.Envoy поддерживает локальные и глобальные ограничения скорости с помощью Rate Limit Service (RLS). Можно настроить ограничения на уровне маршрутов.
Health Checks (проверки работоспособности) — здесь много говорить не стоит, все и так знают, что такое активные проверки здоровья на endpoint. Поддерживает проверки: HTTP, gRPC, L3/L4, Redis и Thrift
Failover (переключение на резервные хосты) — автоматическое переключение трафика на резервные хосты в случае недоступности основного. Envoy поддерживает управление приоритетами в кластерах.
Traffic Shadowing (теневое копирование трафика) — клонирование трафика для тестирования на резервных сервисах. Позволяет отправлять копию реального трафика на тестовый сервис без влияния на основной
Fault Injection (имитация сбоев) — тестирование устойчивости системы при отказах. Вы можете настроить Envoy для имитации задержек или ошибок.
Весь этот список поддерживает envoy и, конечно же, мы его можем реализовать в нашей инфраструктуре. Но разберем мы сегодня не все, но попытаемся охватить, что реально может понадобится в боевой среде.
Установка как docker контейнер
Начнем установку docker pull envoyproxy/envoy: v1.31.3 — звучит очень просто согласитесь? Что нам нужно дальше? Конечно, запустить его, либо как docker-compose
, либо как обычный контейнер. Но я покажу вариант с docker-compose
. Здесь всё обычно: открываем порты под listener на 10000 (он по умолчанию слушает Envoy) и админский интерфейс, откуда мы будем брать метрики. Иначе зачем иметь большую наблюдаемость и не следить за ней?
version: '3.8'
services:
envoy:
image: envoyproxy/envoy:v1.31.3
container_name: envoy
ports:
- "9901:9901"
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml:ro
restart: always
Что нам ещё нужно сделать: создать статический файл конфигурации, поместить его на сервер и через volume подкинуть в контейнер. На этом с запуском контейнера мы закончили.
Ниже — статическая конфигурация по умолчанию. Но есть нюанс: метрики публикуются по пути ip:9901/stats/prometheus
, но и API для управления Envoy также доступно на порту 9901. Если к этому порту будет доступ у любого желающего, то он сможет делать с вашим Envoy всё, что захочет.
Поэтому лучше позаботиться об этом заранее и запустить интерфейс администратора на 127.0.0.1:9901
. На эту тему есть issue.
envoy-demo.yaml
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 10000
protocol: TCP
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io
clusters:
- name: service_envoyproxy_io
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: service_envoyproxy_io
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: www.envoyproxy.io
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: www.envoyproxy.io
Маршрутизация и mTLS между envoy контейнерами
Наглядное изображение, как будет бегать наш трафик между контейнерами
Условно наши два сервиса — service A и service B — общаются между собой по TCP. Что мы можем сделать? Например, для безопасного соединения между сервисами подкинуть mTLS.
static_resources:
listeners:
- name: tcp_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000, protocol: TCP }
filter_chains:
- filters:
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: ingress_tcp
cluster: backend_envoy
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
validation_context:
trusted_ca:
filename: certs/cacert.pem
match_typed_subject_alt_names:
- san_type: DNS
matcher:
exact: serviceA
tls_certificates:
- certificate_chain: { filename: "certs/serverkey.pem" }
private_key: { filename: "certs/servercert.pem" }
clusters:
- name: backend_envoy
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: backend_envoy
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: backend_envoy, port_value: 9001 }
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: serviceB
common_tls_context:
validation_context:
trusted_ca:
filename: certs/cacert.pem
match_typed_subject_alt_names:
- san_type: DNS
matcher:
exact: "*.proxy-example"
tls_certificates:
- certificate_chain: { filename: "certs/clientcert.pem" }
private_key: { filename: "/certs/clientkey.pem" }
Что у нас здесь происходит? Мы сначала объявляем listener 10000 и tcp-фильтр, с помощью которого мы будем управлять нашим трафиком, настраиваем так, чтобы наш Envoy принимал upstream и downstream. DownstreamTlsContext — это контекст TLS, когда подключаются к нам. UpstreamTlsContext — это когда мы роутим трафик на восходящий сервис. Далее происходит следующее: Мы говорим Envoy: проверяй все клиентские сертификаты на входе. Убедись, что ты знаешь об общем центре сертификации и что указан SAN (например, serviceA). Если что-то не совпадает — сбрасывай соединение. Если всё окей, то передай трафик на наш backend_envoy
и также проверь у него SAN (*.proxy-example), SNI (serviceB) и общий CA.
Итого, у нас есть два контейнера. Первый работает на upstream, Второй — на downstream. С более детальными настройками можно ознакомиться здесь и здесь. Мы получили безопасное mTLS-взаимодействие, но немного потеряли в latency — примерно на 2.5 ms.
Балансировка HTTP/2 с фильтрами
В основе конфигурации лежит пример балансировки трафика HTTP/2, но мы можем использовать и другие версии протокола — HTTP/1.1 или HTTP/3. Выбор зависит от возможностей серверов в upstream-кластере и их поддержки определённых протоколов.
static_resources:
listeners:
- name: http_listener
address:
socket_address:
address: 0.0.0.0
port_value: 10000
protocol: TCP
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: http_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
timeout: 15s
cluster: http_backend
retry_policy:
retry_on: "5xx,retriable-4xx"
num_retries: 3
per_try_timeout: 2s
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: http_backend
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
common_http_protocol_options:
idle_timeout: 1h
auto_config:
http_protocol_options: {}
http2_protocol_options:
allow_connect: true
connection_keepalive:
interval: 1s
timeout: 2s
http3_protocol_options:
idle_timeout: 300000ms
quic_protocol_options:
max_concurrent_streams: 100
connection_keepalive:
max_interval: 4s
initial_interval: 150000ms
allow_extended_connect: true
circuit_breakers:
thresholds:
- priority: "DEFAULT"
max_connections: 1024
max_pending_requests: 1024
max_requests: 1024
max_retries: 6
max_connection_pools: 1024
retry_budget:
min_retry_concurrency: 3
outlier_detection:
consecutive_5xx: 5
interval: 10s
base_ejection_time: 30s
max_ejection_time: 300s
max_ejection_percent: 50
successful_active_health_check_uneject_host: false
split_external_local_origin_errors: false
failure_percentage_minimum_hosts: 3
success_rate_minimum_hosts: 3
consecutive_local_origin_failure: 5
enforcing_consecutive_local_origin_failure: 100
enforcing_local_origin_success_rate: 100
health_checks:
- timeout: 2s
interval: 30s
unhealthy_threshold: 3
healthy_threshold: 2
method: "GET"
http_health_check:
path: "/health"
request_headers_to_add:
- header:
key: "Host"
value: "backend_service"
load_assignment:
cluster_name: http_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend_service
port_value: 80
- endpoint:
address:
socket_address:
address: backend_service_2
port_value: 80
- endpoint:
address:
socket_address:
address: backend_service_3
port_value: 80
Что из этого конфига мы можем понять? Мы слушаем все сетевые интерфейсы на порту 10000 по TCP. Для всего трафика который идет по HttpConnectionManager добавляется префикс ingress_http к метрикам. Логи пишем в stdout. Логи можно записывать как в stdout, так и в stderr, в формате JSON или TEXT. Вывод можно настраивать под любые потребности, но по умолчанию мы видим примерно следующее:
stdout log
[2016-04-15T20:17:00.310Z] "POST /api/v1/locations HTTP/2" 204 - 154 0 226 100 "10.0.35.28" "nsq2http" "cc21d9b0-cf5c-432b-8c7e-98aeb7988cd2" "locations" "tcp://10.0.2.1:80"
За подробностями как обычно сюда
Дальше по фильтру: всё, что пришло на /, отправляется в cluster http_backend который имеет в себе 3 конечные точки для балансировки. Установлен глобальный таймаут на upstream — 15 секунд. Если прилетают ошибки 5xx (p.s. тут тоже можно настраивать, на какие именно типы ошибок выполнять повторы), то выполняется 3 повтора с таймаутом 2 секунды на каждый. Если в течение 2 секунд начнётся ответ, счётчик повторов сбрасывается.
Cluster в Envoy — это одновременно список наших конечных точек и конфигурация отказоустойчивости. Таймаут на подключение к конечным точкам установлен в 0.25 секунды. Важно: у каждой системы свои настройки. Здесь мы приводим пример, как это можно сделать. Установлен STRICT_DNS (подробнее можно почитать по ссылке) и метод балансировки round robin. Также включён наш размыкатель Circuit Breakers, который при превышении заданных лимитов позволяет бэкенду «отдохнуть». По дефолту имеет неплохие настройки для всего upstream, но все зависит от того насколько нагружена наша система. Почитать можно по линку.
Circuit Breakers включён по умолчанию. Если хотите его отключить, установите везде значение 1000000000.
Пример из документации
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 1000000000
max_pending_requests: 1000000000
max_requests: 1000000000
max_retries: 1000000000
- priority: HIGH
max_connections: 1000000000
max_pending_requests: 1000000000
max_requests: 1000000000
max_retries: 1000000000
В Envoy блок typed_extension_protocol_options
предоставляет возможность настраивать протоколы HTTP/1.1, HTTP/2 и HTTP/3 для upstream-соединений, адаптируя их под различные сценарии использования. При использовании AutoHttpConfig
кластер автоматически выбирает протокол через ALPN (Application-Layer Protocol Negotiation): HTTP/2 будет использоваться, если он поддерживается, в противном случае — HTTP/1.1. Если upstream-серверы не поддерживают ALPN, Envoy перейдёт на HTTP/1.1. Однако важно, чтобы транспортные сокеты поддерживали ALPN, иначе конфигурация завершится ошибкой. При наличии нестандартных ALPN-настроек Envoy сначала попробует их, но в случае их недоступности переключится на стандартные протоколы HTTP/2 и HTTP/1.1
Общие параметры соединений задаются через common_http_protocol_options
. Например, idle_timeout: 1h
определяет время бездействия, после которого соединение закрывается. Раздел auto_config
позволяет детально настроить параметры для каждого протокола. Для HTTP/1.1 используются стандартные настройки без изменений. В HTTP/2 включён режим allow_connect
, а также настроены интервалы keep-alive: сигнал каждые 1 секунду и таймаут на ответ 2 секунды. Для HTTP/3 добавлены параметры протокола QUIC: ограничение на количество потоков (max_concurrent_streams: 100
), интервалы keep-alive с начальным значением 150000 миллисекунд и максимальным 4 секунды, а также таймаут бездействия 300000 миллисекунд. Включена поддержка расширенного CONNECT, что полезно для проксирования.
Также у нас включены такие фичи как: Outlier detection — пассивная проверка и Health checking — активная. С их помощью мы понимаем, кто из хостов жив, а кто «бьётся в конвульсиях» и ему надо дать полежать, подумать над своим поведением. Split_external_local_origin_errors — очень важная фича. По умолчанию она отключена (false). Когда она выключена, сбросы TCP, таймауты и 5xx ошибки от бэкенда валятся в одну кучу, что условно всё приравнивается к 5xx для Envoy. Если включить (true), то: Ошибки на уровне L4 и L7 начинают считаться отдельно. Таймауты, сбросы TCP и ошибки ICMP идут в одну группу, HTTP-ответы от бэкенда — в другую. Для TCP-роутинга всё немного проще: любая ошибка от TCP-фильтра приравнивается к 5xx как HTTP. successful_active_health_check_uneject_host — по умолчанию true. Это значит: если активная проверка показала, что хост здоров, то Envoy игнорирует все выбросы и считает его рабочим. Это довольно жёсткое поведение, поэтому принимайте решение: нужно ли вам это, или лучше отключить. Про алгоритм выброса можно почитать подробнее, но если вкратце: сначала он смотрит список доступных хостов и % ниже которого нельзя опускаться и после начинает выкидывать «плохие» хосты на период 30s с максимальным значением выброса 5m. После того как хост показывает, что он здоров и готов работать, то он возвращается его в строй и отсчитывается время в обратном порядке до значения которое указано в yaml конфиге.
Panic mode — ключевой механизм балансировки трафика. По умолчанию он активируется, если процент здоровых хостов падает ниже 50%. В этом случае Envoy предполагает, что произошел сбой, и автоматически возвращает в строй все хосты.
У нас есть два управляющих тумблера:
Отключение паники — можно установить порог в 0%, чтобы механизм паники не срабатывал, и все хосты считались доступными
Активация паники — если процент доступных серверов опускается ниже 50%, Envoy заблокирует все хосты и вернет ошибку »503 — no healthy upstream».
Подробнее о настройке panic mode читайте [здесь].
Health check: здесь всё просто. Отправляем GET запрос на /health, добавляем, как пример, заголовок Host: backend_service
и ждём ответа 200. Получили — хорошо, не получили — плохо. Можно настроить проверку как для всего cluster, так и для каждой конечной точки индивидуально. На этом заканчиваем с HTTP и переходим к gRPC. Там расскажу поменьше, так как основные фишки вы уже освоили, но добавлю новых, чтобы не повторяться.
Локальный health check на endpoints
load_assignment:
endpoints:
- lb_endpoints:
- endpoint:
health_check_config:
port_value: 8080
address:
socket_address:
address: 127.0.0.1
port_value: 80
address:
socket_address:
address: localhost
port_value: 80
Балансировка gRPC с фильтрами
gRPC (Remote Procedure Calls) — это система удалённого вызова процедур (RPC) с открытым исходным кодом, первоначально разработанная в Google в 2015 году
static_resources:
listeners:
- name: grpc_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000, protocol: TCP }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc
codec_type: AUTO
route_config:
name: grpc_route
virtual_hosts:
- name: grpc_services
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: grpc_backend
retry_policy:
retry_on: "cancelled,internal,deadline-exceeded"
num_retries: 3
per_try_timeout: 2s
request_mirror_policies:
- cluster: shadow_backend
runtime_fraction:
default_value:
numerator: 100
denominator: HUNDRED
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.grpc_http1_bridge
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_http1_bridge.v3.Config
upgrade_protobuf_to_grpc: true
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_backend
connect_timeout: 1s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options:
allow_connect: true
connection_keepalive:
interval: 1s
timeout: 2s
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 192.168.1.1, port_value: 50051 }
- endpoint:
address:
socket_address: { address: 192.168.1.2, port_value: 50051 }
- endpoint:
address:
socket_address: { address: 192.168.1.3, port_value: 50051 }
- name: shadow_backend
connect_timeout: 1s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: shadow_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 192.168.2.1, port_value: 50052 }
Здесь у нас немного поменялось если сравнивать с HTTP: добавили ошибки, свойственные для gRPC-ответов ("cancelled", "internal", "deadline-exceeded"
), для политики обработки повторов. Также сделали зеркалирование 100% трафика на кластер shadow_backend
, чтобы проверить, как ведёт себя условно новая версия приложения. Можно было поставить split
, чтобы 80% уходило на grpc_backend
, а 20% — на shadow_backend, но я решил показать именно зеркалирование. Добавили два новых фильтра: grpc_web
и grpc_http1_bridge
. Фильтры в envoy обрабатываются по порядку — сверху вниз, за этим нужно обязательно следить.
gRPC-Web — фильтр предназначен для преобразования HTTP/1.1 запросов, совместимых с gRPC-Web, в стандартные HTTP/2 gRPC запросы. Если у вас есть клиент, использующий протокол gRPC-Web (например, веб-приложение в браузере), который не поддерживает полноценный HTTP/2, этот фильтр позволяет преобразовать его запросы так, чтобы они работали с сервером gRPC.
gRPC HTTP1 bridge — фильтр предназначен для преобразования стандартных HTTP/1.1 REST запросов в gRPC-запросы. Если у вас есть клиенты, использующие обычные HTTP/1.1 REST запросы (например, JSON), но сервер поддерживает только gRPC, этот фильтр позволяет использовать эти REST запросы для вызова gRPC серверов. Фича upgrade_protobuf_to_grpc остается везде в положении true, а заголовки application/x-protobuf
будут автоматически преобразованы в gRPC. В этом случае фильтр добавит к телу кадр gRPC, описанный выше, и обновит заголовок content-type до отправки запроса на сервер application/grpc
В случае, если клиент отправляет content-length
заголовок, он будет удален перед продолжением, поскольку значение может конфликтовать с размером, указанным в кадре gRPC.
Тело ответа, возвращаемое клиенту, не будет содержать кадр заголовка gRPC для запросов, обновленных таким образом, т.е. тело будет содержать только закодированный Protobuf.
Аутентификация и авторизация запросов
Итак, Envoy поддерживает JWT Authentication
и OAuth2
. Мы не будем разбирать весь конфигурационный файл, а сосредоточимся на отдельных фрагментах. Как строится конфигурация, думаю, уже ясно, а если возникнут вопросы, всегда можно обратиться к официальной документации. Я расскажу, что можно настроить, а что — нет.
Поддерживаемые jwt алгоритмы
ES256, ES384, ES512, HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, EdDSA
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
path: "/"
route:
cluster: service1
- match:
path: "/api"
route:
cluster: service1
- match:
path: "/health"
route:
cluster: service1
http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
provider1:
issuer: "https://auth.example.com/provider1"
remote_jwks:
http_uri:
uri: "https://auth.example.com/provider1/.well-known/jwks.json"
cluster: auth_cluster
timeout: 5s
forward: true
forward_payload_header: x-jwt-payload
require_expiration: true
cache_duration:
seconds: 300
rules:
- match:
prefix: "/"
requires:
provider_name: provider1
- match:
prefix: "/api"
requires:
provider_name: provider1
- match:
prefix: "/health"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Самое интересное нас ждет в фильтрах, правилах и маршрутах.
Первый фильтр — JwtAuthentication
. Здесь у нас указан issuer
(издатель токена), поле, которое требует обязательного указания времени жизни токена (require_expiration
), и удаленный JWKS, который проверяет наши токены на валидность. JWKS может быть локальным или удаленным. Также в конфигурации указан upstream
-кластер, куда будут уходить запросы.
Дополнительно в конфигурации задано, что после проверки токена его полезную нагрузку необходимо передать в заголовке x-jwt-payload
на upstream
. Однако это опционально, и данное поведение можно отключить. Тумблеров для настройки здесь много, в конце оставлю все ссылки.
По умолчанию JWT токен ищется в заголовке Authorization: Bearer
или в GET-параметре /path?access_token=
. Но, конечно же, это Envoy, поэтому здесь можно настроить все как угодно: указать кастомный заголовок для поиска токена, задать аудитории, которые будут приниматься или отклоняться. Если запрос содержит два токена: один из заголовка и другой из GET-параметра, то оба должны быть валидными.
И самое главное — это маршруты: на каких требуется аутентификация, а куда можно пройти без нее. Например, для /health
аутентификация не требуется, а для остальных маршрутов потребуется предоставить JWT. На каждый маршрут можно указать провайдера, который будет за него ответственным, поскольку их может быть несколько. Для примера я описал только одного провайдера.
Теперь рассмотрим как мы можем авторизировать запросы. Наш фильтр называется envoy.filters.http.oauth2
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http_filters:
- name: envoy.filters.http.oauth2
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
config:
token_endpoint:
cluster: oauth
uri: "https:///auth/realms//protocol/openid-connect/auth"
timeout: 3s
forward_bearer_token: true
use_refresh_token: true
authorization_endpoint: "https:///auth/realms//protocol/openid-connect/auth"
redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback"
redirect_path_matcher:
path:
exact: /callback
signout_path:
path:
exact: /logout
credentials:
client_id: ""
token_secret:
name: token
auth_scopes:
- user
- openid
- email
- name: envoy.filters.http.csrf
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy
filter_enabled:
default_value: true
additional_origins:
- exact: "https://"
- name: envoy.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
codec_type: "AUTO"
stat_prefix: ingress_http
route_config:
virtual_hosts:
- name: service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: service
timeout: 5s
clusters:
- name: service
connect_timeout: 5s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8080
- name: oauth
connect_timeout: 5s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: oauth
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: auth.example.com
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: auth.example.com
Когда пользователь отправляет запрос к защищённому ресурсу через Envoy, процесс авторизации начинается с проверки токена в запросе. Если токен отсутствует или недействителен, Envoy перенаправляет пользователя на страницу авторизации Keycloak, URL которой указан в параметре authorization_endpoint
. Этот запрос включает необходимые параметры, такие как client_id
, указанный в секции credentials
, и запрашиваемые области (auth_scopes
), включая openid
, email
и user
, которые необходимы для выполнения OpenID Connect (OIDC)-аутентификации.
После успешной авторизации Keycloak перенаправляет пользователя на URI, указанный в redirect_uri
. Этот параметр динамически формируется на основе входящего заголовка x-forwarded-proto
и текущего имени хоста (:authority
), что делает систему гибкой для работы в средах с изменяющимися протоколами (HTTP/HTTPS) и доменами. Также указана точка входа для обработки ответа на авторизацию через параметр redirect_path_matcher
, который ожидает, что Keycloak вернёт пользователя на путь /callback
с кодом авторизации.
Когда Envoy получает этот код, он использует его для запроса токена у Keycloak через указанный token_endpoint
. Этот запрос происходит на фоне, где Envoy аутентифицируется перед сервером с помощью идентификатора клиента (client_id
) и секретного ключа (token_secret
). Эти данные обеспечивают безопасное взаимодействие между Envoy и Keycloak.
После получения токена Envoy либо сохраняет его в cookie, либо использует для создания нового запроса к защищённому ресурсу, добавляя токен в заголовок Authorization: Bearer
. Этот токен валидируется при каждом запросе. Для сценариев, где требуется выйти из системы, используется путь signout_path
, который привязан к URI /logout
, позволяя завершить пользовательскую сессию. Когда сервер проверяет клиента и возвращает токен авторизации обратно в фильтр OAuth, независимо от формата этого токена, если forward_bearer_token
установлен в значение true, фильтр отправит cookie с именем BearerToken
в upstream. Кроме того, Authorization
заголовок будет заполнен тем же значением.
use_refresh_token
предоставляет возможность обновлять токен доступа с помощью токена обновления. Если этот флаг отключен, то после истечения срока действия токена доступа пользователь перенаправляется на конечную точку авторизации для повторного входа. Новый токен доступа можно получить с помощью токена обновления без перенаправления пользователя на повторный вход. Для этого необходимо, чтобы токен обновления был предоставлен authorization_endpoint
при входе пользователя в систему. Если попытка получить токен доступа с помощью токена обновления не удалась, пользователь перенаправляется на конечную точку авторизации.
В самом конце фильтр CSRF ограничивает список доменов, откуда могут приходить запросы, обеспечивая дополнительную безопасность.
Из недостатков: для корректной работы фильтра служба должна функционировать по протоколу HTTPS, поскольку файлы cookie используют атрибут ;secure
. Без HTTPS authorization_endpoint
, скорее всего, отклонит входящий запрос, а файлы cookie доступа не будут кэшироваться, что помешает обходу будущих повторных входов в систему
Ссылки для почитать самому: oauth2 и jwt
Envoy — это высокопроизводительный прокси-сервер, созданный для обслуживания современных распределенных систем. Он обеспечивает широкий спектр возможностей, включая маршрутизацию запросов, балансировку нагрузки, управление аутентификацией, обработку JWT-токенов, защиту от атак CSRF, а также интеграцию с внешними системами авторизации, мониторинга, трассировки и еще ОЧЕНЬ много других фильтров.
Одной из ключевых особенностей Envoy является гибкость его настройки. Благодаря модульной архитектуре и множеству фильтров, вы можете адаптировать его под практически любые задачи: от простого маршрутизатора до сложного API-шлюза или элемента mesh-сети. Envoy активно используется в микросервисной архитектуре и служит основой для системы Istio в sidecar mode он же service mesh.
С его помощью можно добиться не только высокой отказоустойчивости, но и гибкого управления трафиком, подробного мониторинга, а также строгого соблюдения правил безопасности. Это делает Envoy мощным инструментом для построения надежной инфраструктуры в условиях современных требований.