Если вы уже перевели часть сервисов в Kubernetes на облачный VDS, то рано или поздно упрётесь в один и тот же вопрос: чем аккуратно и предсказуемо терминировать входящий трафик, когда у вас одновременно есть gRPC и обычный HTTP API, несколько версий сервисов и требования по observability? В какой‑то момент стандартного ingress‑контроллера становится мало, и на сцену выходит Envoy proxy как универсальный API gateway.
В этой статье посмотрим, как использовать Envoy в роли API gateway для gRPC и HTTP API внутри Kubernetes‑кластера на VDS: какую архитектуру выбрать, как описывать listeners, clusters и маршруты, чтобы не запутаться, и какие грабли подстерегают на проде. Никаких сервис‑мэшей «целиком» — только то, что нужно на входе в кластер.
Зачем использовать Envoy как API gateway в Kubernetes
Envoy исторически появился как высокопроизводительный L7‑прокси, и вокруг него уже построено множество решений: Istio, Consul Connect, AWS App Mesh и т.д. Но вам совсем не обязательно тащить весь сервис‑мэш, чтобы использовать Envoy как API gateway на входе в кластер.
Базовые задачи, которые удобно решать через Envoy:
- Единая точка входа для gRPC и HTTP API, без разделения на «специальный» gRPC‑ингресс и классический HTTP‑ингресс.
- Чёткий контроль над маршрутизацией по путям, заголовкам, доменам и режимам
HTTP/HTTP2/gRPC. - Гибкая конфигурация таймаутов, ретраев, circuit breaker'ов, лимитов по подключению и запросам.
- Нормальное логирование и метрики: access‑логи, statsd/Prometheus‑метрики из коробки.
- Постепенный rollout новых версий сервисов (canary, weighted routing) без отдельного ingress‑контроллера.
В отличие от многих ingress‑контроллеров, Envoy вы конфигурируете напрямую (через статический YAML или через xDS). Это усложняет старт, но зато даёт полный контроль над тем, как ходит трафик внутри вашего VDS‑кластера.
Базовая архитектура: Envoy как entrypoint в кластер
Рассмотрим типичную минималистичную схему для одного VDS с Kubernetes (k3s/k0s/managed‑k8s — не принципиально).
Снаружи мир видит только Envoy (NodePort, hostPort или LoadBalancer). Внутри Envoy проксирует запросы на сервисы в кластере: gRPC‑сервисы и HTTP API. TLS можно терминировать на Envoy или пробрасывать до бэкендов.
Условно это выглядит так:
- Клиенты → TCP:443 → Envoy (Deployment + Service типа LoadBalancer/NodePort).
- Envoy listener
0.0.0.0:443сtls_contextиhttp_connection_manager. - Маршрут 1: gRPC API по хосту
grpc.example.comи/или по префиксу/grpc. - Маршрут 2: HTTP REST API по хосту
api.example.comили/api. - Upstream‑сервисы:
grpc-user-service,http-order-serviceи т.п. как обычные Kubernetes Services.
Важно понимать, что в таком сетапе Envoy — не «ещё один слой» поверх ingress‑контроллера, а замена классическому ingress. Это уменьшает магию и повышает предсказуемость: вы сами контролируете, как именно кластера Kubernetes на VDS общаются с внешним миром.

HTTP API и gRPC через один listener
Главный вопрос: нужно ли поднимать отдельные listeners/порты для gRPC и HTTP API? В большинстве сценариев достаточно одного HTTPS‑listener'а с http_connection_manager, который умеет одновременно в HTTP/1.1 и HTTP/2.
Ключевые моменты при совмещении протоколов:
- Для gRPC нужен HTTP/2 end‑to‑end (или хотя бы до gRPC‑бэкенда); Envoy это отлично поддерживает.
- Один и тот же
listenerможет обслуживать и классический HTTP API (REST/JSON), и gRPC с помощью маршрутов. - Envoy автоматически выбирает протокол по ALPN (например,
h2для gRPC,http/1.1для браузерного трафика).
Минимальный пример listener с http_connection_manager в статическом конфиге Envoy (упрощённо):
static_resources:
listeners:
- name: ingress_https
address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filter_chain_match: {}
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
alpn_protocols: ["h2", "http/1.1"]
tls_certificates:
- certificate_chain:
filename: "/etc/envoy/certs/fullchain.pem"
private_key:
filename: "/etc/envoy/certs/privkey.pem"
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
route_config:
name: local_route
virtual_hosts:
- name: grpc_services
domains: ["grpc.example.com"]
routes:
- match:
prefix: "/"
route:
cluster: grpc_user_svc
timeout: 5s
- name: http_api
domains: ["api.example.com"]
routes:
- match:
prefix: "/"
route:
cluster: http_api_svc
timeout: 5s
http_filters:
- name: envoy.filters.http.router
Здесь уже видно разделение по доменам: gRPC‑клиенты ходят на grpc.example.com, HTTP‑клиенты — на api.example.com. Внутри виртуальных хостов вы дальше можете разделять по путям, заголовкам и т.п.
Настройка кластеров dla gRPC и HTTP API
Разница между кластерами для gRPC и HTTP API в Envoy не космическая, но один нюанс критичен: для gRPC очень желательно включить http2_protocol_options на upstream‑кластере, чтобы соединение до бэкенда было HTTP/2.
Примеры двух кластеров (через DNS‑дискавери Kubernetes Services):
clusters:
- name: grpc_user_svc
type: STRICT_DNS
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: grpc_user_svc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: grpc-user-service.default.svc.cluster.local
port_value: 50051
- name: http_api_svc
type: STRICT_DNS
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: http_api_svc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: http-api-service.default.svc.cluster.local
port_value: 8080
Что важно помнить:
STRICT_DNSотлично дружит с Kubernetes + kube-dns/CoreDNS: Envoy периодически резолвит имя сервиса и видит все Pod IP.- Для gRPC‑кластера указываем
http2_protocol_options, иначе upstream будет ходить по HTTP/1.1, и часть фич (streaming) может не работать корректно. connect_timeout,lb_policy, circuit breaker'ы и retry policy настраивайте отдельно для gRPC и HTTP API: у них часто разные ожидания по ретраям.
Envoy в Kubernetes: Deployment и Service
С конфигом всё понятно, но как именно запускать Envoy в Kubernetes‑кластере на VDS? Чаще всего это отдельный Deployment в namespace, например, edge или ingress.
Базовый подход:
Deploymentс Envoy‑контейнером, конфиг монтируется какConfigMap.ServiceтипаLoadBalancer,NodePortилиClusterIP + hostPort(в зависимости от того, как вы публикуете кластер во внешний мир на VDS).- Сертификаты TLS монтируются из
Secret(или из файловой системы, если вы интегрируете с cert-manager через volume).
Упрощённый пример манифеста Deployment и Service (без TLS‑секретов и readiness‑hooks, только суть):
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy-gateway
namespace: edge
spec:
replicas: 2
selector:
matchLabels:
app: envoy-gateway
template:
metadata:
labels:
app: envoy-gateway
spec:
containers:
- name: envoy
image: envoyproxy/envoy:v1.32-latest
args:
- "-c"
- "/etc/envoy/envoy.yaml"
ports:
- containerPort: 443
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
volumes:
- name: envoy-config
configMap:
name: envoy-config
---
apiVersion: v1
kind: Service
metadata:
name: envoy-gateway
namespace: edge
spec:
type: NodePort
selector:
app: envoy-gateway
ports:
- name: https
port: 443
targetPort: 443
nodePort: 30443
На VDS этот Service типа NodePort вы можете подсветить наружу через iptables или nftables, либо через L4‑балансировщик, если у вас несколько нод. Для одиночного узла часто проще пробросить порт на host‑сети (через hostPort или MetalLB), но это уже детали конкретной инсталляции Kubernetes.
Маршрутизация gRPC‑методов и HTTP путей
Для HTTP API всё привычно: match по prefix или path, заголовкам, хосту. Для gRPC есть отдельный фильтр envoy.filters.http.grpc_web (если вы используете grpc-web) и поддержка маршрутизации по служебным заголовкам.
Несколько практических паттернов маршрутизации:
- Использовать разные домены для gRPC и HTTP API (
grpc.example.comиapi.example.com) — меньше путаницы. - Если домен один, разделяйте по префиксам:
/grpc/...и/api/.... - Для grpc-web добавляйте фильтр
grpc_webпередrouter.
Пример маршрутизации по префиксам:
route_config:
name: local_route
virtual_hosts:
- name: main
domains: ["api.example.com"]
routes:
- match:
prefix: "/grpc/user."
route:
cluster: grpc_user_svc
- match:
prefix: "/api/"
route:
cluster: http_api_svc
gRPC‑клиент указывает метод в виде /package.Service/Method. Envoy может маршрутизировать по этому строковому пути (если вы хотите, например, разные бэкенды для разных версий API). Но на практике чаще используют разделение по доменам или префиксам, чтобы не городить сложные safe_regex‑матчи.
Если нужна именно grpc-web‑схема, можно дополнительно посмотреть материал про интеграцию в статье grpc-web через Envoy: конфигурация и подводные камни.
Таймауты, ретраи и circuit breakers
Для API gateway на основе Envoy особо важны таймауты и лимиты — вы становитесь «вратами» в свой кластер, и именно здесь легче всего отрезать «шумные» или ошибочные запросы, чем давать им заваливать бэкенды.
Ключевые места настройки:
- Таймаут на уровне маршрута (
route.route.timeout) — общий лимит жизни запроса. - Retry policy для отдельных маршрутов (например, для идемпотентных GET).
- Лимиты подключений и запросов на уровне кластера (
circuit_breakers).
Простейший пример повышенного таймаута для медленного gRPC‑метода и запрета ретраев:
routes:
- match:
prefix: "/grpc/report."
route:
cluster: grpc_report_svc
timeout: 60s
retry_policy:
num_retries: 0
Для HTTP API наоборот часто хотят ретраи для 5xx и сетевых ошибок, но только для идемпотентных методов:
- match:
prefix: "/api/"
route:
cluster: http_api_svc
timeout: 10s
retry_policy:
retry_on: "5xx,connect-failure,refused-stream"
num_retries: 2
per_try_timeout: 3s
Не забывайте, что ретраи на уровне Envoy плюс ретраи в клиенте и в приложении легко превращаются в «шквал» запросов при деградации, поэтому политику нужно продумывать для каждого маршрута отдельно.
Observability: логирование и метрики
Одно из главных преимуществ Envoy как API gateway — прозрачное наблюдение за всем, что приходит в ваш кластер Kubernetes на VDS: и gRPC, и HTTP API.
Минимальный набор, который стоит включить сразу:
- Access‑лог в JSON для дальнейшей отправки в Loki, ELK или ClickHouse.
- Статистика Envoy (Prometheus или StatsD) — RPS, ошибки, latency по кластерам.
- grpc‑специфичные метрики (например, коды ошибок gRPC).
Пример JSON access‑лога в http_connection_manager:
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/var/log/envoy/access.log"
log_format:
json_format:
protocol: "%PROTOCOL%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
authority: "%REQ(:AUTHORITY)%"
response_code: "%RESPONSE_CODE%"
grpc_status: "%GRPC_STATUS%"
duration_ms: "%DURATION%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
upstream_host: "%UPSTREAM_HOST%"
user_agent: "%REQ(USER-AGENT)%"
x_request_id: "%REQ(X-REQUEST-ID)%"
Такой формат удобно парсить любым лог‑коллектором, и, что важно, здесь сразу есть grpc_status и обычный HTTP‑код. Это позволяет различать, например, gRPC UNAVAILABLE и HTTP 503 на уровне дашбордов.

Типичные грабли и отладка
С gRPC и HTTP API через Envoy обычно всплывают одни и те же проблемы. Ниже — наиболее частые и что с ними делать.
1. gRPC не работает, а HTTP API работает
Чаще всего причины такие:
- Нет HTTP/2 на upstream‑кластере — забыли
http2_protocol_options. - Клиент не договаривается по ALPN (например, старый gRPC‑клиент без TLS или с неправильными настройками).
- Маршрут ловит gRPC‑запрос как обычный HTTP и ломает заголовки.
Что проверить в первую очередь:
- В access‑логах поля
protocolиgrpc_status. - В stats Envoy — счётчики
cluster.<name>.upstream_cx_protocol_error. - На бэкенде — видит ли он HTTP/2 (например, через debug‑лог сервера приложения).
2. Внезапные таймауты и обрывы стримов
Особенно это заметно для долго живущих gRPC‑стримов. Возможные источники:
- Слишком маленький
idle_timeoutили общийtimeoutмаршрута. - Load balancer или firewall перед Envoy, который режет долгие соединения.
- Переиспользование соединения с изменением конфигурации Envoy (reload без учёта активных стримов).
Что можно сделать:
- Увеличить или отключить
idle_timeoutдля конкретных gRPC‑маршрутов. - Явно задать
max_connection_durationи учитывать его поведение на клиенте. - Аккуратно планировать rolling update Envoy, следя за drain‑процессом и метриками соединений.
3. Высокая латентность при первом запросе
Классика: первый gRPC‑ или HTTP‑запрос после простоя долго идёт. Возможные причины:
- DNS‑резолвинг кластера (
STRICT_DNS) и холодные TCP‑коннекты. - Низкое значение
preconnect_ratioили его отсутствие. - Connection pool Envoy простаивал и теперь собирает новые соединения.
Отладка и смягчение:
- Посмотреть метрики connection pool на upstream‑кластерах (active, ready, busy коннекты).
- Включить пререндеринг коннектов или увеличить минимальное количество соединений.
- При необходимости оптимизировать схему HTTP/2 и h2c, в том числе на стороне бэкендов; детальнее это разобрано в статье HTTP/2 и h2c для upstream‑сервисов за Envoy и Nginx.
Стратегия развертывания и обновлений
Так как Envoy — точка входа в весь кластер, его обновления на VDS особенно чувствительны. Пара практических рекомендаций:
- Используйте
Deploymentс типом обновленияRollingUpdateи небольшим шагом (один pod за раз), чтобы не отрезать весь трафик при проблеме с новым образом. - Перед выкатыванием новой версии конфигурации гоняйте её через
envoy --mode validate -c path/to/envoy.yamlв CI. - Добавьте readinessProbe, который проверяет внутренний endpoint Envoy или факт загрузки конфигурации.
- Сохраняйте обратную совместимость маршрутов (например, не удаляйте старые кластеры, пока вы не убедились, что весь трафик переведён).
Для более продвинутых сценариев (динамическая маршрутизация, canary‑релизы без правки YAML) имеет смысл посмотреть в сторону xDS‑серверов или сервис‑мэш‑решений, но это уже отдельная тема.
Итоги
Использование Envoy как API gateway для gRPC и HTTP API в Kubernetes‑кластере на VDS позволяет получить предсказуемый, управляемый входной слой: единый listener, точную маршрутизацию, понятные логи и метрики. Да, придётся повозиться с конфигами и tooling'ом, но взамен вы получите API‑вход, который легко масштабировать и эволюционировать вместе с вашим кластером.
Если вы только начинаете, стартуйте с простого статического конфига: один listener, пара кластеров, минимальный набор маршрутов и логов. Когда это стабилизируется на прогоне нагрузки, постепенно внедряйте таймауты, retry‑policy, circuit breaker'ы и продвинутые паттерны маршрутизации. В этом подходе главное — не пытаться «сделать Istio за вечер», а шаг за шагом превращать Envoy в надёжный фасад вашего Kubernetes‑кластера на VDS. А сами кластера при этом можно безболезненно переносить между тарифами или площадками, так как внешний entrypoint остаётся единым.


