Содержание урока · 15 разделов
- grpc web и envoy grpc: обещание, данное конфигом
- Зачем gRPC-серверу прокси
- Словарь Envoy: listener, routes, cluster
- http2_protocol_options: почему без него не поедет
- TLS-терминация: шифруем до прокси, дальше периметр
- grpc web: мост для браузера через прокси
- Ingress: backend-protocol GRPC
- Health-check прокси: спросить grpc.health.v1
- Четыре ноги одного маршрута
- Чек-лист развертывания gRPC за прокси
- Практика: что собрать локально
- Что дальше: итоговый проект
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
grpc web и envoy grpc: обещание, данное конфигом#
В финале прошлого урока была строка-обещание: двадцать четвёртый урок спускается в инфраструктуру конкретно, Envoy покажут конфигом, а не обзором. Конфиг ниже — реальный, минимальный и рабочий: от него один шаг до продакшн-версии, и всё, что в нём появится, вы уже умеете объяснять к концу урока. Зачем вообще это нужно? Пока клиент вашего gRPC-сервиса — другой Go-процесс, серверу хватает порта 50051 из четвёртого урока. Но как только в списке потребителей появляются браузер, мобильное приложение, ingress и проверки здоровья — у сервера появляется слой, который не должен жить в Go-коде. Запрос «envoy grpc» в поиске — это почти всегда история про один и тот же зазор: между HTTP/2-клиентом снаружи и вашим сервером внутри кто-то должен стоять, и этот кто-то обязан понимать gRPC до конца, включая трейлеры. Разберём четыре вещи. Первая — словарь Envoy: listener, routes, cluster, upstream, и почему gRPC-проксирование требует http2_protocol_options на ноге к серверу. Вторая — TLS-терминация: шифруем до прокси, внутри периметра plaintext, и это осознанный компромисс из двенадцатого урока. Третья — grpc web: мост для браузера через grpc_web_filter и честное сравнение с Connect. Четвёртая — ingress с аннотацией backend-protocol и health-пробы прокси по grpc.health.v1. Бизнес-код во всех этих историях не меняется — проверяйте сами: ни один YAML этого урока не трогает хендлеры.
Зачем gRPC-серверу прокси#
Прежде чем разбирать YAML, зафиксируем, что прокси покупает, а чем платим. Покупает три вещи. Первая — единая граница: один публичный порт и один сертификат на все сервисы, вместо того чтобы выдавать TLS-адреса каждому процессу; смена сертификата превращается из передеплоя Go-сервиса в перезапуск прокси. Вторая — протокольные мосты: браузер, который мы уже знаем по прошлому уроку как клиента без доступа к трейлерам, дотягивается до сервиса только через прокси-режим gRPC-Web — и это задача прокси, а не сервера. Третья — фильтры и проверки: ограничение путей, CORS для браузерных клиентов, активные health-пробы — всё это конфигурация, которую можно менять без пересборки сервиса. Теперь цена. Прокси — это лишний hop: плюс латентность на каждый вызов, плюс процесс, который тоже падает, обновляется и потребляет память. И — главное, почему не всякий reverse proxy годится: gRPC опирается на трейлеры HTTP/2, а их умеют прозрачно проводить очень немногие прокси. Документация Envoy говорит об этом в лоб: «gRPC makes use of trailers to convey request status. Envoy is one of very few HTTP proxies that correctly supports trailers and is thus one of the few proxies that can transport gRPC requests and responses» — gRPC передаёт статус вызова в трейлерах, и Envoy — один из очень немногих HTTP-прокси, которые поддерживают трейлеры корректно и потому вообще могут переносить gRPC-запросы и ответы. Сама природа протокола та же, что мы знаем с первого урока: «At the transport layer it uses HTTP/2 or above for request/response multiplexing» — на транспортном уровне HTTP/2 и мультиплексирование запросов и ответов. Выбор Envoy здесь не вкусовщина: это прокси, спроектированный под HTTP/2 с первого дня.
Словарь Envoy: listener, routes, cluster#
Конфигурация Envoy пугает объёмом, но строится из пяти слов, которые стоит выучить сразу — дальше весь урок будет на них держаться. Listener — точка входа: IP-адрес и порт, на которых прокси принимает соединения; всё, что клиент видит снаружи, начинается с listener-а. Внутри listener-а — цепочка сетевых фильтров, и для HTTP-трафика она почти всегда состоит из одного фильтра: http_connection_manager (HCM), который разбирает HTTP и решает, что делать с запросом дальше. HCM смотрит в routes — таблицу маршрутов: виртуальные хосты с доменами и правилами вида «путь с таким префиксом — в такой cluster». Для gRPC путь запроса самодостаточен: метод вызова уезжает по HTTP/2 в пути вида /пакет.Сервис/Метод, поэтому одно правило с префиксом /shop.v1.OrderService/ накрывает все методы сервиса сразу — ни один маршрут не надо описывать руками. Cluster — набор upstream-эндпоинтов, куда Envoy отправит вызов: конкретные адрес и порт вашего gRPC-сервера. Термины downstream и upstream задают направление: downstream — сторона клиента, upstream — сторона сервера; почти каждая ловушка этого урока живёт на одной из этих двух ног. И последнее слово — фильтры HTTP: цепочка, через которую проходит каждый запрос внутри HCM, где router — обязательный последний элемент, который собственно отправляет запрос в cluster. Конфигурация бывает статической (всё в одном файле) и динамической — через xDS, когда listener-ы и cluster-ы приезжают от управляющего сервера; мы работаем со статической, потому что механика у них одна.
Базовый конфиг Envoy перед gRPC-сервером
GO# envoy.yaml — минимальный Envoy перед gRPC-сервером orders
static_resources: # статическая конфигурация: всё в одном файле
listeners:
- name: grpc_listener # точка входа: сюда стучится клиент или ingress
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
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_hcm
codec_type: HTTP2 # клиенты говорят HTTP/2 — gRPC иначе не бывает
route_config:
name: orders_routes
virtual_hosts:
- name: orders
domains: ["*"] # отвечаем на любое имя хоста
routes:
# путь RPC = /пакет.Сервис/Метод, префикс накрывает все методы
- match: { prefix: "/shop.v1.OrderService/" }
route:
cluster: shop_orders # имя cluster-а ниже
http_filters: # цепочка HTTP-фильтров; router — всегда последний
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: shop_orders
connect_timeout: 1s
type: STATIC # endpoints заданы здесь; бывают DNS и EDS
http2_protocol_options: {} # ключ урока: upstream говорит HTTP/2
load_assignment:
cluster_name: shop_orders
endpoints:
- lb_endpoints:
- endpoint:
address:
# gRPC-порт из четвёртого урока — сервер не меняется
socket_address: { address: 127.0.0.1, port_value: 50051 }Читаем сверху вниз по словарю из предыдущего блока. Listener на 8080 принимает соединения; единственный сетевой фильтр — http_connection_manager; codec_type: HTTP2 разрешает HTTP/2 со стороны клиента, потому что gRPC-клиент другого протокола просто не умеет. route_config содержит один virtual_host с доменом «*» и одно правило: всё, что начинается с /shop.v1.OrderService/, уходит в cluster shop_orders — так маршрутизация выглядит для всех методов сервиса сразу. В clusters — единственный endpoint, 127.0.0.1:50051: ваш grpc-сервер, не изменённый ни на строку. Строка http2_protocol_options: {} пока выглядит необязательной — она не выглядит ни на что не похожей, просто пустые настройки; на самом деле это самый важный ключ всего урока, и следующий блок — про него. Заметьте и то, чего здесь нет: TLS, health-проверок, grpc_web_filter — мы добавим их по одному в следующих блоках, каждый — отдельной строкой конфига.
http2_protocol_options: почему без него не поедет#
Самая частая ошибка первого Envoy-конфига перед gRPC: всё написано правильно, listener работает, маршруты сходятся — а клиент получает ошибки протокола, и unary-вызовы, и стримы. Причина в ноге к серверу: по умолчанию Envoy говорит с upstream по HTTP/1.1, а gRPC требует HTTP/2. Логика Envoy здесь строгая, и документация cluster-а объясняет её дословно: «Even if default HTTP2 protocol options are desired, this field must be set so that Envoy will assume that the upstream supports HTTP/2 when making new HTTP connection pool connections» — даже если вам нужны настройки HTTP/2 по умолчанию, поле должно быть выставлено явно, чтобы Envoy считал upstream говорящим на HTTP/2 при создании новых соединений. Дальше — ещё честнее: «Currently, Envoy only supports prior knowledge for upstream connections» — сейчас Envoy поддерживает только prior knowledge для upstream-соединений: никаких догадок и автоматического апгрейда, сервер обязан быть HTTP/2-объявленным заранее. И третья деталь, которая валит продовые конфиги: «Even if TLS is used with ALPN, http2_protocol_options must be specified» — даже если используется TLS с ALPN, http2_protocol_options обязателен: сам факт TLS-соединения HTTP/2 до бэкенда не включает. Симптом на клиенте — недетерминированные UNAVAILABLE и обрывы вместо внятного ответа: сервер grpc-go ждёт HTTP/2-преамбулу, а получает HTTP/1.1-запрос. Одним абзацем честности: поле http2_protocol_options в новых версиях считается устаревшим в пользу общего механизма HttpProtocolOptions через typed_extension_protocol_options — но семантика та же самая: «upstream обязан говорить HTTP/2», и в тысячах существующих конфигов вы встретите именно эту форму. Выучить нужно принцип, форму легко поменять. И бонус, важный для конца урока: та же строка документации замечает — «As an aside this allows HTTP/2 connections to happen over plain text» — это попутно разрешает HTTP/2-соединения поверх открытого текста: h2c, на котором строится вся TLS-терминация ниже.
Проверка: забыли http2_protocol_options
В конфиге Envoy указаны listener, маршруты и cluster на gRPC-сервер 50051, но http2_protocol_options забыли. Что увидит клиент?
TLS-терминация: шифруем до прокси, дальше периметр#
TLS-терминация — это когда шифрованное соединение заканчивается на прокси: он расшифровывает трафик, применяет свои правила и дальше идёт к серверу как обычный HTTP/2-клиент. Базу вы умеете из урока про TLS: там вы собирали TLS вручную — tls.Config, сертификат на сервере, взаимная аутентификация по CN. Теперь у границы появился профессиональный терминатор, и собранное там не выбрасывается — наоборот: именно потому, что вы понимаете, что проверяет рукопожатие, вы способны рассудить, что именно делает прокси. Конфигурационно это две независимые строки: transport_socket с сертификатом на listener-е (нога клиент—прокси) и — ничего — на cluster-е (нога прокси—сервер остаётся plaintext). Легальность plaintext-ноги — не баг, а свойство: мы уже читали в документации cluster-а, что http2_protocol_options «As an aside this allows HTTP/2 connections to happen over plain text» — HTTP/2 поверх открытого текста разрешён, и это ровно тот h2c, по которому Envoy пойдёт к вашему серверу. Такой расклад — стандартный компромисс периметра: сертификат и его обновление живут в одном месте, серверный код не знает о TLS вовсе, а дальше работает правило «внутри периметра доверенной сети plaintext — границу охраняет сама граница». Честность требует назвать цену: терминатор видит все заголовки и все сообщения — metadata, токены, содержимое вызовов; компрометация прокси означает компрометацию трафика. Если этот риск неприемлем, вариантов два, и оба вам уже знакомы: закрыть ногу прокси—сервер TLS из двенадцатого урока вручную или отдать её mesh-у прошлого урока с его mTLS между сайдкарами. Заодно закрывается мост к двадцать второму уроку: keepalive-политики между прокси и сервером теперь настраиваются в конфиге прокси — ловушка too_many_pings из прошлого урока при переезде под прокси живёт там же.
TLS на listener, h2c в cluster
GO# listener из базового конфига — теперь с TLS-терминацией
listeners:
- name: grpc_listener_tls
address:
socket_address: { address: 0.0.0.0, port_value: 8443 }
filter_chains:
- transport_socket: # TLS живёт здесь, не в Go-коде сервера
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "/etc/certs/server.crt" }
private_key: { filename: "/etc/certs/server.key" }
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_tls_hcm
codec_type: AUTO # ALPN сам выберет h2 для gRPC-клиента
route_config:
name: orders_routes
virtual_hosts:
- name: orders
domains: ["*"]
routes:
- match: { prefix: "/shop.v1.OrderService/" }
route: { cluster: shop_orders } # upstream остаётся h2c
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.RouterСравните с базовым конфигом: поменялись три места, и все — на клиентской ноге. Порт 8443 вместо 8080, транспортный сокет с сертификатом на filter_chain — это и есть терминирование: расшифровка происходит до того, как запрос увидит http_connection_manager, — и codec_type: AUTO, при котором Envoy по ALPN выбирает протокол для каждого соединения: gRPC-клиент договорится на h2, обычный HTTP-клиент получит HTTP/1.1. Нога к серверу не изменилась ни на букву: cluster shop_orders как говорил http2_protocol_options, так и говорит — plaintext h2c внутри периметра. Сертификат в примере — серверный, как в двенадцатом уроке; взаимная аутентификация по клиентскому сертификату добавляется в common_tls_context тем же способом, которым вы добавляли её в tls.Config, только в YAML.
Проверка: где заканчивается шифрование
Envoy терминирует TLS: нога клиент—прокси зашифрована, нога прокси—сервер — plaintext h2c. Что это значит для модели доверия?
grpc web: мост для браузера через прокси#
Теперь обещанная история про браузер. Базу вы уже знаете: в connect-курсе есть целый урок «Что такое Connect и gRPC-Web», где подробно разобрано, почему браузерный JavaScript не может говорить на нативном gRPC — fetch не даёт доступа к HTTP/2-трейлерам, где живёт grpc-status, — и как на эту боль отвечает Connect, который обслуживает три протокола из одного хендлера. Здесь мы смотрим на ту же проблему с другой стороны: не глазами серверного разработчика, а глазами прокси. Протокол gRPC-Web придуман самой gRPC-командой, и его спецификация PROTOCOL-WEB.md из репозитория grpc с первого абзаца говорит, кто главный потребитель: «This protocol is designed to make it easy for a proxy to translate between the protocols as this is the most likely deployment model» — протокол спроектирован так, чтобы прокси было легко переводить между протоколами, потому что именно прокси — самая вероятная схема развертывания. Из тех же design goals: «decouple from HTTP/2 framing which is not, and will never be, directly exposed by browsers» — отвязаться от HTTP/2-фрейминга, который браузеры никогда не покажут напрямую. Механика на стороне прокси — фильтр grpc_web_filter, чья страница в документации Envoy коротка: «This is a filter which enables the bridging of a gRPC-Web client to a compliant gRPC server» — фильтр, соединяющий gRPC-Web-клиента с gRPC-сервером. Обзорная страница уточняет механику: «allows a gRPC-Web client to send requests to Envoy over HTTP/1.1 and get proxied to a gRPC server» — gRPC-Web-клиент может прийти к Envoy даже по HTTP/1.1 и получить проксирование к gRPC-серверу. Что именно переводит фильтр? Главное отличие протокола — статус уезжает в теле: «Response status encoded as part of the response body»; по спецификации «8th (MSB) bit of the 1st gRPC frame byte» помечает фрейм как трейлеры, а не как данные, и «Trailers must be the last message of the response, as enforced by the implementation» — трейлеры всегда последнее сообщение ответа. Фильтр упаковывает grpc-status в тело на обратном пути — и браузер, читающий тело, наконец видит статус вызова.
grpc_web_filter в цепочке фильтров
GO# http_filters из http_connection_manager: мост для браузера
http_filters:
- name: envoy.filters.http.grpc_web # gRPC-Web → gRPC и обратно
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.router # router — всегда последний в цепочке
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.RouterФильтр добавляется в цепочку http_filters до router-а — и это всё, что нужно для поддержки gRPC-Web: фильтр сам распознаёт Content-Type application/grpc-web (+proto или +json) у входящих запросов и application/grpc у ответов upstream-а, конвертируя оба направления. Порядок в цепочке значим: фильтры применяются последовательно, и router, который отправляет запрос в cluster, обязан быть последним — фильтр, поставленный после router-а, никогда не выполнится. Для старых браузеров и XHR-клиентов существует текстовый режим: Content-Type application/grpc-web-text, где тело дополнительно закодировано в base64 — это тот самый случай, когда байтовый поток обязан проехать через транспорт, не переваривающий бинарщину. Заметьте, что server-код по-прежнему не тронут: grpc-go-сервер по-прежнему говорит только нативный gRPC, вся конвертация живёт в прокси.
Что лежит в последнем фрейме тела
Прочитайте код и выберите, что он выведет:
// браузер: fetch к gRPC-Web-эндпоинту Envoy
const resp = await fetch("/shop.v1.OrderService/GetOrder", {
method: "POST",
headers: { "Content-Type": "application/grpc-web+proto" },
body: reqBytes, // запрос: длина + сообщение, трейлеров у клиента нет
});
// тело ответа по spec gRPC-Web:
// [5-байтовый префикс][сообщение][фрейм с первым байтом 10000000b]
// Первый байт последнего фрейма — 10000000b. Что внутри?Ingress: backend-protocol GRPC#
Envoy из этого урока — прокси, который вы конфигурируете вручную. В Kubernetes на границе кластера чаще стоит ingress-controller: объект Ingress описывает, какой хост и путь в какой сервис ведёт, а контроллер (nginx-ingress — самый распространённый) превращает это в конфиг своего прокси. По смыслу это тот же слой, что Envoy выше: listener на 443, маршрутизация по хосту и пути, TLS-терминация по k8s-секрету. Отличие — в умолчаниях: nginx по умолчанию говорит с бэкендом по HTTP/1.1, что для gRPC означает ровно ту же ловушку, что мы разбирали выше, — и лечится аннотацией. Документация ingress-nginx про неё дословно: «Using backend-protocol annotations is possible to indicate how NGINX should communicate with the backend service» — аннотация говорит NGINX, как общаться с бэкендом, — и дальше: «Valid Values: HTTP, HTTPS, AUTO_HTTP, GRPC, GRPCS and FCGI», а также «By default NGINX uses HTTP» — по умолчанию HTTP/1.1, то есть без аннотации gRPC-вызовы до пода не доедут. Значение GRPC включает HTTP/2 к бэкенду без TLS — тот самый h2c из TLS-терминации: внешний сертификат терминируется самим ingress-controller, а нога к поду остаётся plaintext внутри кластера. Значение GRPCS — то же самое, но с TLS и до пода: берите его, когда периметру мало, — серверный сертификат при этом живёт в k8s-секрете и монтируется в под, механика рукопожатия всё та же из двенадцатого урока. Честная оговорка: ingress-controller — это самостоятельный прокси со своим набором слов; Envoy-фрагменты этого урока — словарь Envoy, nginx — словарь nginx, но деление на ноги «снаружи TLS, внутри plaintext» и требование HTTP/2 до gRPC-бэкенда у них общие.
Ingress с backend-protocol: GRPC
GO# ingress.yaml — nginx-ingress перед сервисом orders
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: orders-grpc
annotations:
# к поду — HTTP/2 gRPC без TLS; без этой аннотации будет HTTP/1.1
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
spec:
ingressClassName: nginx
tls: # внешний TLS терминируется на ingress-controller
- hosts: ["orders.example.com"]
secretName: orders-tls # сертификат в k8s-секрете
rules:
- host: orders.example.com
http:
paths:
- path: /shop.v1.OrderService/
pathType: Prefix
backend:
service:
name: orders # Service перед gRPC-подами
port:
number: 50051Всё то же самое, что в Envoy-конфиге, только словами Kubernetes. Listener — это сам ingress-controller на 443; маршруты — правила host + path с тем же префиксом /shop.v1.OrderService/; cluster — backend: Service orders на порт 50051. TLS-секция с секретом orders-tls — внешний сертификат, терминируемый контроллером; аннотация backend-protocol: "GRPC" — нога к поду: HTTP/2 gRPC plaintext. При желании ступеньку ingress и ступеньку Envoy из этого урока совмещают: ingress терминирует TLS и разруливает пути, а Envoy с grpc_web_filter живёт за ним — но это уже топология на вкус команды, механика у каждой ступени ровно та, что разобрана выше.
Что откроет nginx к поду
Прочитайте код и выберите, что он выведет:
// ingress.yaml (из блока выше):
// nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
// spec.tls: { hosts: [orders.example.com], secretName: orders-tls }
// backend: { service: orders, port: 50051 }
//
// Браузерный gRPC-Web-клиент шлёт запрос на https://orders.example.com/...
// Какое соединение NGINX откроет к Service orders:50051?Health-check прокси: спросить grpc.health.v1#
Семнадцатый урок дал вашему серверу health-сервис: grpc.health.v1 с Check и Watch, SetServingStatus на жизненном цикле. С тех пор у него накопились потребители: Kubernetes-пробы, healthCheckConfig канала из шестнадцатого урока — и теперь ещё прокси. Идея активной проверки: Envoy не ждёт, пока живой трафик наткнётся на мёртвый бэкенд, а сам по расписанию шлёт каждому endpoint-у запрос и по ответу решает, держать ли его в ротации. Формат проверки — знакомый: документация health_check.proto описывает grpc_health_check как «grpc.health.v1.Health-based healthcheck» — проверку на базе grpc.health.v1.Health, того самого протокола из семнадцатого урока. Поле service_name в ней объяснено честно: «An optional service name parameter which will be sent to gRPC service in grpc.health.v1.HealthCheckRequest message» — необязательное имя сервиса, которое уедет в HealthCheckRequest; пустое значение спрашивает статус сервера целиком, конкретное — статус одного сервиса, как в SetServingStatus. Пороги делают проверку устойчивой к дребезгу: два провала подряд выводят endpoint из ротации, два успеха подряд возвращают. Отличите активные проверки от пассивных: outlier detection — это статистика по реальному трафику (ошибки и таймауты реальных вызовов), его Envoy включает рядом, но они требуют трафика, тогда как активные пробы работают даже на простаивающем бэкенде. В прошлом уроке health уже был координатором релизного конвейера — здесь он выполняет ту же работу на ступени прокси: неготовый под, выведенный в NOT_SERVING, перестаёт получать трафик от прокси до восстановления. Один контракт, ещё один потребитель — ничего нового серверу отдавать не нужно.
Активные пробы в cluster
GO# cluster с активными проверками по grpc.health.v1
clusters:
- name: shop_orders
connect_timeout: 1s
type: STATIC
http2_protocol_options: {} # health-пробы тоже ездят по HTTP/2
load_assignment:
cluster_name: shop_orders
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 50051 }
health_checks:
- timeout: 1s # сколько ждём ответа на Check
interval: 5s # частота проб
unhealthy_threshold: 2 # два провала подряд — endpoint из ротации
healthy_threshold: 2 # два успеха подряд — обратно
grpc_health_check: # grpc.health.v1 из семнадцатого урока
service_name: "shop.v1.OrderService" # уедет в HealthCheckRequesthealth_checks — поле cluster-а, поэтому проба проверяет именно endpoints этого cluster-а, а не абстрактный «сервис». Прокси отправляет Check из grpc.health.v1 каждые пять секунд; ответ SERVING держит endpoint в ротации, NOT_SERVING и таймаут ведут счётчики порогов. service_name сопоставьте с тем, что вы регистрировали в health-сервере на Go из семнадцатого урока: если хендлер ставит статус для shop.v1.OrderService — спрашивайте именно его, иначе получите статус целиком сервера или NOT_SERVING из-за несовпадения имени. Обратите внимание: http2_protocol_options нужен и здесь — пробы отправляются через то же upstream-соединение, и без HTTP/2 health-чекер не заработает ровно по тем же причинам, что и обычный трафик.
Путь браузерного gRPC-Web-запроса
Соберите путь одного вызова из браузера до gRPC-сервера и обратно — девять шагов, каждый прыжок — новая ступень конфига из этого урока.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Соберите cluster для gRPC
Вставьте пропущенные имена: ключ HTTP/2 у upstream-а, таймаут пробы и поле с именем сервиса в grpc.health.v1.
Проверка: gRPC-Web или Connect
Фронтенд-команда просит методы вашего grpc-go-сервиса из браузера. Чем gRPC-Web принципиально отличается от Connect с точки зрения развертывания?
Четыре ноги одного маршрута#
| Нога | Протокол | TLS |
|---|---|---|
| клиент → ingress | HTTP/2 gRPC или gRPC-Web по решению ingress | терминируется на ingress-controller: внешний сертификат из k8s-секрета |
| ingress → под | HTTP/2 gRPC при аннотации backend-protocol: GRPC | plaintext h2c: шифрование закончилось на границе кластера |
| Envoy → gRPC-сервер | HTTP/2 при http2_protocol_options — иначе не поедет | plaintext h2c внутри периметра, как разрешено prior knowledge |
| браузер → Envoy | gRPC-Web: Content-Type application/grpc-web, статус в теле | TLS до прокси: тот же listener с transport_socket |
| сервис → сервис | нативный gRPC | по ситуации: TLS из двенадцатого урока руками или mTLS mesh-а |
TLS-терминация экономит сертификаты, код и время релиза, но её цена — прозрачный трафик внутри периметра: компрометация любого узла между прокси и сервером означает чтение всех сообщений, включая metadata с токенами. Такой компромисс легален до тех пор, пока периметр действительно охраняется — сетевые политики, сегментация, минимальные права. Проверяйте себя одним вопросом: «что произойдёт, если этот plaintext-сегмент увидит посторонний?» Если ответ «ничего хорошего» — закрывайте ногу прокси—сервер TLS из двенадцатого урока или отдавайте её mesh-у прошлого урока. И помните про keepalive: прокси-серверная нога — тоже HTTP/2-соединение, политики пингов из двадцать второго урока теперь конфигурируются в прокси, а не в Go-коде.
Чек-лист развертывания gRPC за прокси#
- На cluster-е к gRPC-серверу стоит http2_protocol_options — без него ничего не поедет, даже с TLS и ALPN
- TLS-терминация на границе — осознанное решение: plaintext-нога закрыта периметром, а иначе TLS из двенадцатого урока или mesh
- Для браузерных клиентов grpc_web_filter стоит в http_filters до router-а, router — всегда последний
- Статус вызова в gRPC-Web уезжает в теле: последний фрейм с битом 10000000b — трейлеры, fetch их читает
- Ingress с gRPC-бэкендом объявляет backend-protocol: GRPC — иначе nginx пойдёт на под по HTTP/1.1
- GRPCS вместо GRPC — когда TLS нужен и до пода: сертификат в k8s-секрете на серверной стороне
- Активные пробы cluster-а спрашивают grpc.health.v1 с тем же service_name, что зарегистрирован в health-сервере
- Каждая новая ступень проксирования — плюс латентность и минус гарантия: держите цепочку короткой и измеряйте
Практика: что собрать локально#
Честная оговорка про формат, одна на урок: в самоучителе есть песочница с интерпретатором Go, но сеть в браузере недоступна — прокси, TLS-сертификаты и ingress-controller в ней не поднять, поэтому runner-ов в этом уроке, как и во всём gRPC-курсе, нет; проверяют вас quiz, predict, fill, order и карточки, а руками делайте локально — всё по силам вечеру. Эксперимент первый, Envoy: возьмите сервер из четвёртого урока, запустите Envoy с конфигом из этого урока (в docker это одна команда с -v на envoy.yaml) и пройдите grpcurl-ом через 8080; затем уберите http2_protocol_options и убедитесь, что всё сломалось — лучшей демонстрации важности этого ключа не придумать. Эксперимент второй, gRPC-Web: добавьте grpc_web_filter в http_filters и напишите десяток строк на JavaScript с fetch из predict-задания; в devtools найдите последний фрейм тела и проверьте бит 10000000b. Эксперимент третий, TLS: выпустите самоподписанный сертификат openssl-ом как в двенадцатом уроке, повесьте на listener transport_socket и сравните две ноги tcpdump-ом или в логах: снаружи — шифрованные байты, внутри — читаемый HTTP/2. Эксперимент четвёртый, health: выведите сервер в NOT_SERVING через SetServingStatus и посмотрите по admin-логам Envoy, как endpoint выйдет из ротации после двух провалов и вернётся после восстановления.
Что дальше: итоговый проект#
Инфраструктурный слой курса закрыт: сервер не менялся весь урок, зато у него появились listener, routes и cluster, TLS на границе, мост для браузера и пробы по grpc.health.v1 — всё в YAML, всё без единой строки бизнес-кода. Остаётся применить всё сразу: двадцать пятый урок — итоговый проект, двусторонний чат и файловый сервис на bidi-стримах и стриминговых загрузках, где вам понадобится весь курс разом: контракты из второго урока, стримы из седьмого и девятого, дедлайны из четырнадцатого, тесты из двадцать первого. С одним предупреждением из этого урока: стримы через прокси едут, но bidi через gRPC-Web ограничен платформой браузера — нативные клиенты проезжают всё, браузерным клиентам честно проектируйте границы. Полный маршрут курса со всеми уроками — в оглавлении gRPC-курса: до финала один шаг.
Карточки урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Прокси перед gRPC-сервером покупает границу: единый порт и сертификат, мосты протоколов, фильтры и пробы — ценой hop-а и ещё одного процесса; Envoy здесь не вкусовщина, а один из немногих прокси, корректно проводящих трейлеры.
- Словарь Envoy: listener — точка входа, HCM с routes матчит пути RPC по префиксу, cluster — upstream-эндпоинты; http_filters — цепочка, где router всегда последний.
- http2_protocol_options на cluster-е обязателен: Envoy говорит с upstream по HTTP/1.1, поддерживает только prior knowledge и не включает HTTP/2 даже при TLS с ALPN — без этого ключа gRPC не поедет.
- TLS-терминация: сертификат на listener-е, нога к серверу — plaintext h2c; это осознанный компромисс периметра, который закрывают TLS из двенадцатого урока или mesh-ом, а keepalive-политики ноги живут в конфиге прокси.
- gRPC-Web — протокол, спроектированный под прокси: grpc_web_filter переводит браузерные запросы в нативный gRPC и складывает статус в тело (фрейм с битом 10000000b); Connect решает ту же задачу без прокси — три протокола из одного хендлера.
- Ingress с backend-protocol: GRPC говорит nginx-у включить HTTP/2 к поду; активные пробы cluster-а спрашивают grpc.health.v1 из семнадцатого урока — один health-контракт обслуживает k8s, канал и прокси.
Домашнее задание
Разверните orders-сервис из курса за Envoy: возьмите сервер с health-сервисом из семнадцатого урока, поднимите Envoy с конфигом из этого урока — listener, маршрут по префиксу /shop.v1.OrderService/, cluster с http2_protocol_options и grpc_health_check с правильным service_name. Пройдите grpcurl через прокси, затем уберите http2_protocol_options и опишите симптомы. Добавьте grpc_web_filter и проверьте вызов из браузерного fetch: найдите в теле ответа финальный фрейм с битом 10000000b и выпишите grpc-status. Выпустите самоподписанный сертификат, перенесите TLS на listener и убедитесь, что нога к серверу осталась h2c. В конце напишите абзац честности: где в вашей топологии заканчивается шифрование, кто это компрометирует и что вы готовы сделать — TLS до сервера или mesh.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Envoy, а не просто nginx перед gRPC-сервером?
Главный критерий — трейлеры: gRPC передаёт статус в трейлер-фреймах HTTP/2, и документация Envoy прямо говорит, что это один из очень немногих прокси, которые поддерживают трейлеры корректно и потому могут переносить gRPC. Классический nginx исторически поддерживал HTTP/2 только на клиентской ноге и не умел h2c к бэкенду — для gRPC это блокер. Плюс Envoy из коробки даёт grpc_web_filter для браузеров и grpc_health_check по grpc.health.v1. Если же вы в Kubernetes и нужен именно ingress — nginx-ingress с аннотацией backend-protocol: GRPC закрывает базовый случай.
http2_protocol_options устарел — что использовать вместо него?
В новых версиях Envoy поле считается устаревшим в пользу общего механизма HttpProtocolOptions через typed_extension_protocol_options — там HTTP/2 до upstream-а задаётся в explicit_http_config. Семантика не изменилась ни на шаг: upstream обязан говорить HTTP/2, prior knowledge остаётся единственным способом, plaintext h2c разрешён. Поэтому в существующих конфигах вы встретите обе формы, и читать нужно принцип: ключ урока не в имени поля, а в требовании HTTP/2 на ноге к серверу.
Чем gRPC-Web отличается от Connect и что выбрать для браузера?
gRPC-Web — протокол с прокси-моделью развертывания: grpc-go-сервер его не говорит, между браузером и сервером нужен Envoy с grpc_web_filter, который переводит протоколы и складывает статус в тело ответа. Connect — другой стек, где один хендлер на net/http обслуживает gRPC, gRPC-Web и Connect одновременно без прокси, с читаемыми ответами и более простым отладчиком. Остаться на grpc-go и прокси — берите gRPC-Web; готовы рассмотреть другой стек — смотрите connect-курс, там этот выбор разобран подробно, начиная с урока «Что такое Connect и gRPC-Web».
Где заканчивается шифрование при TLS-терминации и опасно ли это?
На прокси: нога клиент—прокси зашифрована, нога прокси—сервер — plaintext h2c, легальный HTTP/2 поверх открытого текста. Терминатор видит все заголовки и все сообщения, поэтому компрометация прокси или сегмента между прокси и сервером означает компрометацию трафика, включая metadata с токенами. Это стандартный компромисс периметра, и он осознан: сертификат живёт в одном месте, серверный код не знает о TLS. Если риск неприемлем — закрывайте ногу прокси—сервер TLS из двенадцатого урока или отдайте её mesh-у прошлого урока с mTLS между сайдкарами.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Connect: современный RPC от Buf
Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.
- Строить сервисы на connectrpc.com/connect
- Понимать три протокола и выбирать правильный
- Настраивать buf: lint, breaking changes, generate
- Писать interceptors и обрабатывать ошибки