---
title: "Развертывание и проксирование"
description: "Envoy перед gRPC-сервером: http2_protocol_options, TLS-терминация, grpc_web_filter для браузера, ingress с backend-protocol GRPC и health-check прокси."
course: "gRPC-Go: высокопроизводительный RPC"
url: "https://golangmaster.ru/courses/grpc-go/lessons/24-proksirovanie/"
published: "2026-09-19"
updated: "2026-09-26"
minutes: 20
---

# Развертывание и проксирование

*Курс «gRPC-Go: высокопроизводительный RPC», урок 24 из 25. ~20 минут. Обновлено 26.09.2026. Источник: https://golangmaster.ru/courses/grpc-go/lessons/24-proksirovanie/*

Прошлый [урок](/courses/grpc-go/lessons/23-microservices/) обещал, что «Envoy покажут конфигом, а не обзором», — уроки, в которых обещают конфиг, обязаны его показать. Сегодня это ровно то, что мы делаем: настоящий YAML Envoy, который слушает порт, разбирает пути RPC и отправляет вызовы на ваш gRPC-сервер, — а вместе с ним словарь проксирующего мира, без которого тот YAML не читается. Заодно закроем три вопроса, которые прошлый двадцать третий урок сознательно оставил за скобками. Первый — почему связка «envoy grpc» не работает из коробки: gRPC требует HTTP/2 до upstream, а прокси по умолчанию говорит с бэкендом на HTTP/1.1, и эту разницу надо включить руками. Второй — запрос «grpc web»: как браузер, у которого нет доступа к трейлерам, всё-таки дотягивается до gRPC-сервиса через grpc_web_filter, и когда вместо этого честнее взять Connect. Третий — ingress: nginx-ingress с аннотацией backend-protocol, TLS-терминация на границе и активные health-пробы прокси по тому самому grpc.health.v1 из семнадцатого урока. Разворачивать сервис мы учимся так, чтобы бизнес-код не менялся ни на строку: всё, что появится в этом уроке, живёт в YAML и манифестах, а не в main.go.

### 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 забыли. Что увидит клиент?

- ✅ Ошибки протокола и обрывы вызовов: Envoy говорит с сервером по HTTP/1.1, а gRPC требует HTTP/2, который включается только prior knowledge
- Ничего страшного: Envoy сам договорится с сервером по ALPN и включит HTTP/2
- Unary-вызовы поедут, сломаются только стримы — HTTP/1.1 якобы не умеет многосообщенные ответы
- Все вызовы вернут NotFound: без HTTP/2 маршруты не матчатся

Документация cluster-а формулирует это дословно: поле должно быть выставлено, чтобы Envoy считал upstream говорящим на HTTP/2, prior knowledge — единственный поддержанный способ, и даже TLS с ALPN его не заменяет. По умолчанию нога к серверу — HTTP/1.1, а grpc-сервер ждёт HTTP/2-преамбулу: отсюда ошибки протокола и UNAVAILABLE вместо внятного ответа. ALPN тут не спасает — это ловушка из текста выше. Стримы не «сломаются отдельно»: ломается сам протокол, а не часть вызовов. Маршруты не при чём — матчинг происходит до отправки в cluster.

### TLS-терминация: шифруем до прокси, дальше периметр

TLS-терминация — это когда шифрованное соединение заканчивается на прокси: он расшифровывает трафик, применяет свои правила и дальше идёт к серверу как обычный HTTP/2-клиент. Базу вы умеете из [урока про TLS](/courses/grpc-go/lessons/12-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. Что это значит для модели доверия?

- ✅ Шифрование заканчивается на прокси: дальше данные защищены только периметром сети — это осознанный компромисс, который при необходимости закрывают TLS из двенадцатого урока или mesh
- Шифрование сквозное: прокси пересылает зашифрованные байты как есть, не имея к ним доступа
- Plaintext внутри периметра запрещён спецификацией gRPC — так делать нельзя
- Ничего не меняется: TLS всегда заканчивается на сервере, просто у прокси есть копия ключа

Терминация — это именно терминация: рукопожатие TLS завершается на прокси, и дальше до сервера идёт открытый HTTP/2 — легальный h2c, разрешённый той самой строкой документации про plain text. Сквозное шифрование противоречит самой идее терминирования: прокси обязан видеть заголовки, чтобы маршрутизировать и фильтровать запросы. Спецификация gRPC ничего не запрещает — plaintext между доверенными узлами легален и повсеместен. Сервер в этой схеме вообще не знает про TLS: никакого «ключа у прокси» нет, есть два независимых соединения. Поэтому честная формулировка компромисса: граница видит всё, и если этого недостаточно — возвращайтесь к двенадцатому уроку или к mesh-у прошлого.

### grpc web: мост для браузера через прокси

Теперь обещанная история про браузер. Базу вы уже знаете: в connect-курсе есть целый [урок «Что такое Connect и gRPC-Web»](/courses/connect/lessons/01-vvedenie/), где подробно разобрано, почему браузерный 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, вся конвертация живёт в прокси.

### Что лежит в последнем фрейме тела

```go
// браузер: 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. Что внутри?
```

**Что выведет программа?**

- ✅ Трейлеры: grpc-status и grpc-message, закодированные в теле ответа, — настоящий HTTP/2-трейлер браузер прочитать не может, поэтому статус уезжает в теле
- Сжатое protobuf-сообщение: 10000000b — флаг сжатия, дальше compressed-байты
- Копия HTTP-заголовков ответа, продублированных в тело на случай прокси
- Пустышка-маркер конца файла: данные уже прочитаны, этот фрейм ничего не несёт

Спецификация gRPC-Web вводит ровно эту схему: старший (восьмой) бит первого байта gRPC-фрейма отличает трейлеры от данных — 10000000b это uncompressed trailer как часть тела. «Response status encoded as part of the response body» — статус уезжает в теле: пары grpc-status/grpc-message кодируются как HTTP/1-заголовочный блок, и «Trailers must be the last message of the response» — трейлеры обязаны быть последним сообщением. Именно так решается проблема, с которой начинается урок: браузер не видит HTTP/2-трейлеры, поэтому grpc_web_filter складывает статус в тело, где тело fetch-у уже доступно. Флаг сжатия — это другой бит того же префикса, заголовки в тело не дублируются, а пустышек в протоколе нет.

### 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 к поду

```go
// 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?
```

**Что выведет программа?**

- ✅ HTTP/2 gRPC без TLS: аннотация GRPC включает h2c-ногу к поду, внешний TLS терминирован на самом ingress — шифрование закончилось на границе
- HTTP/2 gRPC с TLS: значение GRPC означает gRPC поверх TLS, сертификат из orders-tls поедет к поду
- HTTP/1.1 с заголовком Upgrade: nginx сам апгрейдит соединение до HTTP/2
- TLS-passthrough: nginx передаст зашифрованные байты поду, не расшифровывая

Значение GRPC — это HTTP/2 к бэкенду без TLS; для gRPC поверх TLS до пода существует отдельное значение GRPCS из списка допустимых значений аннотации. Внешний сертификат из секции tls терминируется самим ingress-controller — под его не получает, и нога к Service остаётся plaintext внутри кластера: это та же модель периметра, что и у Envoy выше. Никаких апгрейдов nginx не делает — протокол к бэкенду задаётся аннотацией заранее, а passthrough в ingress-nginx — отдельная схема (ssl-passthrough), в которой весь смысл TLS-терминации теряется. Проверьте себя: из четырёх вариантов ровно один соответствует и значению аннотации, и модели доверия периметра.

### Health-check прокси: спросить grpc.health.v1

[Семнадцатый урок](/courses/grpc-go/lessons/17-health/) дал вашему серверу 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" # уедет в HealthCheckRequest
```

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

Правильный порядок строк:

1. `браузер шлёт fetch POST /shop.v1.OrderService/GetOrder с Content-Type application/grpc-web+proto — трейлеров у клиента нет, статус придёт в теле`
2. `TLS-соединение заканчивается на listener-е Envoy: сертификат проверен, трафик расшифрован — дальше периметр прокси`
3. `http_connection_manager разбирает запрос и ведёт его по цепочке http_filters: первым стоит grpc_web_filter`
4. `grpc_web_filter видит Content-Type application/grpc-web и переводит запрос в нативный gRPC: Content-Type application/grpc, HTTP/2-фрейминг`
5. `routes матчит префикс /shop.v1.OrderService/ и направляет вызов в cluster shop_orders`
6. `Envoy открывает upstream-соединение к 127.0.0.1:50051 по HTTP/2 — http2_protocol_options в cluster-е включён`
7. `grpc-go-сервер отвечает как обычному gRPC-клиенту: сообщения и трейлеры с grpc-status в HTTP/2`
8. `grpc_web_filter на обратном пути упаковывает трейлеры в тело: последний фрейм с битом 10000000b несёт grpc-status`
9. `браузер читает тело fetch-ом и достаёт статус вызова — трейлеров HTTP/2 ему не понадобилось`

### Соберите cluster для gRPC

Вставьте пропущенные имена: ключ HTTP/2 у upstream-а, таймаут пробы и поле с именем сервиса в grpc.health.v1.

```go
# Envoy cluster перед gRPC-сервером: HTTP/2 и активные пробы
clusters:
  - name: shop_orders
    connect_timeout: 1s
    type: STATIC
    ___: {} # upstream обязан говорить HTTP/2 — иначе gRPC не поедет
    health_checks:
      - ___: 1s # сколько ждём ответа на Check
        interval: 5s
        unhealthy_threshold: 2
        grpc_health_check:
          ___: "shop.v1.OrderService" # имя сервиса в HealthCheckRequest
```

Подсказка: Первый — главный ключ урока с подчёркиваниями в имени; второй — поле из словаря пробы рядом с interval; третий — поле HealthCheckRequest из семнадцатого урока.

Правильные ответы в пропусках: `http2_protocol_options`, `timeout`, `service_name`

### Проверка: gRPC-Web или Connect

**Вопрос:** Фронтенд-команда просит методы вашего grpc-go-сервиса из браузера. Чем gRPC-Web принципиально отличается от Connect с точки зрения развертывания?

- ✅ gRPC-Web без прокси не живёт: нужен Envoy с grpc_web_filter перед grpc-go-сервером; Connect-хендлер на net/http отвечает сам и понимает gRPC, gRPC-Web и Connect одновременно
- gRPC-Web работает без прокси, а Connect требует Envoy — наоборот
- Разницы в развертывании нет: оба протокола grpc-go поддерживает из коробки
- Connect требует TLS на сервере, а gRPC-Web работает и на plaintext

grpc-go-сервер говорит только нативный gRPC, поэтому путь gRPC-Web — всегда через прокси с grpc_web_filter: Envoy переводит gRPC-Web в gRPC и складывает статус в тело ответа. Connect выбирает другой путь: один хендлер на net/http сам обслуживает три протокола, прокси не обязателен — подробный разбор и когда какой стек уместен, оставлен connect-курсу. Есть и мост между ними: у Envoy существует connect_grpc_bridge — «allows Connect requests to be sent to Envoy. Envoy then translates the requests to gRPC to be sent to the upstream» — перевод Connect-запросов в gRPC до upstream-а. TLS тут ни при чём: и тот, и другой путь работает на plaintext и на TLS — вопрос в том, где живёт перевод протоколов.

### Четыре ноги одного маршрута

| Нога | Протокол | 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-а |

> **Plaintext внутри периметра — решение, а не случайность.** 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-курса](/courses/grpc-go/): до финала один шаг.

### Карточки урока

- **listener, routes, cluster** — Словарь Envoy: listener — точка входа (адрес и порт); HCM разбирает HTTP и смотрит в routes — таблицу путей; cluster — набор upstream-эндпоинтов. Для gRPC путь вида /пакет.Сервис/Метод, поэтому префикс накрывает все методы сервиса. downstream — сторона клиента, upstream — сторона сервера.
- **http2_protocol_options** — Ключ cluster-а, включающий HTTP/2 до upstream-а. Без него Envoy говорит по HTTP/1.1 и gRPC не работает: Envoy поддерживает только prior knowledge, даже TLS с ALPN его не заменяет. Разрешает h2c — HTTP/2 поверх plaintext, основу TLS-терминации. Новая форма — HttpProtocolOptions через typed_extension_protocol_options.
- **TLS-терминация** — Шифрованное соединение заканчивается на прокси: transport_socket с сертификатом на listener-е, нога прокси—сервер — plaintext h2c. Терминатор видит заголовки и сообщения. Компромисс периметра; если мало — TLS из двенадцатого урока или mTLS mesh-а. keepalive-политики ноги — конфиг прокси.
- **grpc_web_filter** — HTTP-фильтр Envoy в цепочке http_filters до router-а: мост gRPC-Web → gRPC. gRPC-Web-клиент может прийти по HTTP/1.1; статус вызова уезжает в теле ответа — последний фрейм с битом 10000000b несёт grpc-status/grpc-message. Есть текстовый режим application/grpc-web-text с base64.
- **backend-protocol: GRPC** — Аннотация nginx-ingress: как NGINX говорит с бэкендом. Значения HTTP, HTTPS, AUTO_HTTP, GRPC, GRPCS и FCGI; по умолчанию HTTP. GRPC — HTTP/2 gRPC без TLS к поду (h2c), GRPCS — gRPC поверх TLS до пода. Без аннотации gRPC-вызовы к поду не доезжают.
- **grpc_health_check** — Активная проба Envoy: health_checks в cluster-е шлёт Check из grpc.health.v1 с интервалом; пороги unhealthy/healthy берегут от дребезга. service_name уезжает в HealthCheckRequest: пустое — статус сервера, конкретное — одного сервиса из SetServingStatus. Отличать от пассивного outlier detection по реальному трафику.

## Коротко: что запомнить

- Прокси перед 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 между сайдкарами.
