✻ Урок 5.4 · Тема 5: Kubernetes и Helm
Вход в кластер: Ingress и Gateway API
Содержание урока
Зачем это нужно
Service из урока 5.3 живёт только внутри кластера: снаружи до его адреса не добраться. А пользователь приходит с браузером, по имени вроде notes.lab, на порты 80 и 443, и ему нужен HTTPS (защищённое соединение с замочком: данные между браузером и сайтом зашифрованы, и посторонний в сети их не прочитает). Значит, кто-то должен принять соединение снаружи, расшифровать его, выбрать нужный сервис по имени сайта и пути в адресе и передать запрос дальше. Без такого «входа» каждый сервис пришлось бы выставлять наружу отдельным адресом и отдельно настраивать шифрование.
В 2026 на собеседованиях спрашивают не «что такое Ingress», а «почему ingress-nginx больше не выбирают для нового проекта и чем его заменяют». Ingress (входное правило) это старый способ описать такой вход: YAML-объект, в котором написано «запросы на такой-то сайт отправляй в такой-то сервис». Он заморожен: новых возможностей в него не добавляют. Самый популярный его исполнитель ingress-nginx (программа, которая по таким правилам настраивает nginx) выведен из поддержки, то есть уязвимости в нём больше не закрывают. Новые кластеры строят на Gateway API (новый стандарт описания входа, то же назначение, но устроен удобнее, разберём ниже). Ты поставишь Envoy Gateway (программу, которая настраивает прокси Envoy по описаниям Gateway API; прокси это посредник, через которого проходят чужие запросы), опишешь вход тремя небольшими манифестами (YAML-файлами с описанием объектов) и разберёшь три типовые поломки.
Шаг проекта: «Заметки» открываются снаружи кластера по http(s)://notes.lab через Gateway notes-gw и маршрут HTTPRoute notes. Gateway (шлюз) это объект, который описывает «двери» входа: на каких портах и для каких имён кластер принимает трафик. HTTPRoute (HTTP-маршрут) это объект с правилами «запросы на такое имя и такой путь отправь в такой сервис». Подробно разберём в теории ниже.
Что нужно знать
- Урок 5.1: свой кластер kind: кластер
notes, в котором порты 80 и 443 твоего компьютера проброшены на порты узла 30080 и 30443. - Урок 5.2: Deployment: три пода
notes, метки, командыgetиdescribe. - Урок 5.3: Service и DNS кластера: Service
notesна порту 8080, список endpoints (адресов подов за сервисом),kubectl port-forward. - Урок 2.5: nginx как reverse proxy: обратный прокси (reverse proxy) это программа-посредник, которая принимает запросы от пользователей и передаёт их нужным серверам за ней; здесь то же самое, но на уровне кластера.
- Урок 2.6: TLS и HTTPS: сертификат (электронный «паспорт» сайта), ключ, SAN (список имён сайта, вписанный в сертификат; подробно ниже), самоподписанный сертификат (выданный самим себе, браузер ему не доверяет).
- Урок 2.4: HTTP: заголовок
Host, коды 404, 502, 503.
Картина целиком
Представь бизнес-центр. В нём десятки фирм, у каждой свой кабинет, но входная дверь одна. У двери стоит охранник (это вход в кластер). Посетитель говорит: «Мне в компанию Notes». Охранник смотрит в журнал, находит, куда идти, и провожает. Он же проверяет пропуск: без него не пустит. Фирмы не выставляют своих охранников на улицу, а журнал ведёт администрация здания.
- Здание это кластер, кабинеты это сервисы.
- Охранник это программа-посредник (прокси), которая принимает все запросы снаружи. В нашем курсе это Envoy.
- Журнал «кому куда» это правила маршрутизации: HTTPRoute (или старый Ingress). Маршрутизация значит «решить, куда отправить запрос».
- Администрация, которая решает, какие двери есть вообще (порты, сертификаты), это Gateway.
- Тот, кто вообще нанял охранников определённой фирмы, это GatewayClass (запись «какой именно программой обслуживаются входы») и контроллер (программа, которая читает описания и настраивает охранника; в нашем случае это Envoy Gateway).
flowchart TD
B["Браузер: http://notes.lab/notes"] -->|"1. имя notes.lab = 127.0.0.1<br>(через --resolve или /etc/hosts)"| H["Порт 80 компьютера"]
H -->|"kind пробрасывает"| N["Порт 30080 узла кластера"]
N --> E["Envoy: прокси внутри кластера"]
E -->|"2. читает Host: notes.lab и путь /notes,<br>смотрит в правила HTTPRoute"| S["Service notes :8080<br>(из урока 5.3)"]
S --> P["один из трёх подов notes"]
Оговорка: в здании охранник один. Envoy в кластере тоже может работать в нескольких копиях, поэтому «охранник» это скорее роль, чем один процесс.
Теория
Как трафик попадает в кластер
Поды и ClusterIP снаружи недоступны: это адреса внутренней сети кластера. Есть три способа впустить трафик.
- NodePort. Узел (node) это машина, на которой работает кластер; в kind каждый узел это контейнер Docker (урок 5.1). Сервис типа NodePort открывает один и тот же порт (из диапазона 30000-32767) на каждом узле, и всё, что пришло на этот порт, попадает в сервис. Просто, но порты неудобные, а разбора по имени сайта нет: один порт это один сервис.
- LoadBalancer. Кластер просит у облака выделить внешний балансировщик с публичным адресом. Удобно, но каждый такой балансировщик стоит денег и обслуживает один сервис. В kind его нет.
- Общий вход (reverse proxy, обратный прокси). В кластере работает одна программа, которая слушает порты 80 и 443, читает из HTTP-запроса заголовок
Host(какое имя сайта ввёл клиент, см. урок 2.4) и путь (/notes) и раздаёт запросы по разным сервисам. Это тот же nginx из урока 2.5, только настраивается не файлом, а объектами Kubernetes.
Общий вход лучше всего: снаружи один адрес и одна пара портов, а внутри сколько угодно сервисов, и все остаются на дешёвом ClusterIP. Именно такой вариант мы строим.
Разобранный пример. Пусть у компании три сайта: notes.lab, wiki.lab, shop.lab. С LoadBalancer нужно три внешних адреса и три счёта. С общим входом нужен один адрес: запрос на wiki.lab приходит на тот же порт 80, прокси видит Host: wiki.lab и отправляет его в сервис wiki.
Важная деталь: объект сам ничего не делает. В Kubernetes ты не запускаешь прокси, а описываешь желаемое: «хочу вход для notes.lab». Это записывается объектом в базу кластера. Работу выполняет контроллер (controller): программа внутри кластера, которая следит за такими объектами и настраивает настоящий прокси. Нет контроллера: объект создан, kubectl не ругается, а трафика нет. Так же устроены Deployment и Service, но там контроллеры встроены в Kubernetes. Для входа контроллер ставишь ты сам.
Прикинь сам: у компании три сайта, и все нужно открыть наружу. Сколько внешних адресов и счетов понадобится с LoadBalancer на каждый сайт и сколько с общим входом?
Три адреса и три счёта против одного адреса. Общий вход различает сайты по заголовку Host, а сервисы за ним остаются на дешёвом ClusterIP.
Осторожно: NodePort тоже впускает трафик, но один порт ведёт в один сервис, без разбора по имени сайта.
Проверь понимание: ты создал Ingress,
kubectl get ingressпоказывает его, аcurlне отвечает. Что первым делом проверишь?
Ответ
Есть ли в кластере Ingress-контроллер и подходит ли ingressClassName (имя класса, по которому объект выбирает контроллер). Объект Ingress сам по себе трафик не пропускает, его должен подхватить контроллер. Признак: пустая колонка ADDRESS.
Главное: снаружи достаточно одного общего входа; объект с описанием сам трафик не пропускает, его должен подхватить контроллер.
Чтобы общий вход раздавал запросы по сервисам, ему нужны признаки запроса: вот какие.
Как прокси выбирает сервис: заголовок Host и путь
Как Envoy понимает, что запрос предназначен именно notes, если на порт 80 приходят запросы для всех сайтов? По двум признакам, которые есть в каждом HTTP-запросе.
- Заголовок
Host. Браузер, открываяhttp://notes.lab/notes, сначала превращает имя в IP-адрес (через DNS), а затем шлёт запрос по этому адресу и добавляет в него строкуHost: notes.lab. Адрес нужен, чтобы дойти до машины, аHostнужен, чтобы машина знала, какой из сайтов на ней имеется в виду. - Путь (path): часть адреса после имени, здесь
/notes.
Разберём запрос по строкам, как он выглядит «на проводе»:
GET /notes HTTP/1.1 <- метод, путь и версия протокола
Host: notes.lab <- для какого сайта запрос
User-Agent: curl/8.5.0 <- кто спрашивает
Accept: */* <- какие ответы принимает
Прокси сопоставляет эти две вещи с правилами. Правило hostnames: [notes.lab] совпадёт с запросом только если в Host именно это имя. Отсюда две практические вещи, на которых ты будешь ловиться:
- Если обратиться к входу просто по IP (
curl http://127.0.0.1/healthz), вHostокажется127.0.0.1, правило не совпадёт, и придёт404. Прокси жив, он просто не знает, какому сайту адресован запрос. - Ключ
--resolve notes.lab:80:127.0.0.1вcurlрешает это. Значение читается какимя:порт:адрес: «когда пойдёшь наnotes.labпорт 80, не спрашивай DNS, а иди на 127.0.0.1».curlзаменяет DNS-запрос своим ответом, но вHostостаётсяnotes.lab. Это почти то же самое, что добавить строку127.0.0.1 notes.labв файл/etc/hosts, только без правки системных файлов.
Тип совпадения пути. В правиле маршрута type: PathPrefix со значением / совпадает с любым путём, который начинается с /, то есть со всеми. Если бы значение было /api, совпали бы /api и /api/notes, но не /apiary, потому что префикс сравнивается по целым сегментам между косыми чертами. Есть и Exact: точное совпадение, только /api и ничего больше. Если правил несколько, побеждает самое точное (самый длинный путь), поэтому общее правило / можно смело держать на всякий случай последним.
Осторожно, путаница: hostnames в HTTPRoute и hostname в listener это разные поля. Listener говорит, какие имена шлюз вообще принимает, а маршрут говорит, какие из них относятся к нему. Маршрут будет принят, только если эти два множества пересекаются.
Прикинь сам: ты вызвал
curl http://127.0.0.1/notesбез--resolve. Что ответит Envoy и почему?
404: в заголовке Host окажется 127.0.0.1, правило для notes.lab не совпадёт. Прокси жив, он просто не знает, какому сайту адресован запрос.
Главное: прокси выбирает маршрут по
Hostи пути;--resolveподменяет DNS, но оставляет вHostнужное имя.
Правила для таких запросов писали в Ingress, и вот почему он остановился в развитии.
Ingress: что это и почему он застыл
Ingress (networking.k8s.io/v1) это первый стандартный объект для входа. Он описывает правила «хост и путь ведут в Service». Вот минимальный пример:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: notes-demo
namespace: notes
spec:
ingressClassName: nginx # какой контроллер должен обработать этот объект
rules:
- host: notes.lab # для какого имени сайта правило
http:
paths:
- path: / # для каких путей
pathType: Prefix # Prefix = все пути, начинающиеся с /
backend:
service:
name: notes # отправить в Service notes
port:
number: 8080
Читается легко, и в этом сила. Но умеет он мало: маршрутизацию по хосту и пути и TLS для хоста. Всё остальное (переписывание пути, таймауты, лимиты, редиректы) контроллеры реализовали через аннотации (annotations): это поля-заметки в metadata, которые Kubernetes не разбирает, а читает только конкретный контроллер. У каждого контроллера свои. Манифест, написанный под ingress-nginx, под Traefik не заработает.
Вторая проблема: один объект Ingress описывает и точку входа, и маршруты. Чтобы команде приложения поправить маршрут, приходится давать права на объект, где живут и сертификаты. В большой компании это неудобно и небезопасно.
Что с ним стало. Ingress не удалён и будет жить годами, но развитие остановлено: новые возможности в него не добавляют. Самый популярный контроллер, ingress-nginx, архивирован и выведен из поддержки в марте 2026 (проверь актуальный статус на странице проекта): новые уязвимости в нём не закрываются. Новые кластеры строят на Gateway API. А читать Ingress ты обязан уметь: в старых кластерах он везде.
Прикинь сам: манифест Ingress с аннотациями под ingress-nginx ты применил в кластер, где стоит Traefik. Заработают ли аннотации?
Нет: аннотации читает только конкретный контроллер, у каждого свои. Объект создастся, но нужные настройки Traefik не увидит.
Осторожно: Ingress не удалён и годами будет встречаться в старых кластерах, поэтому читать его нужно уметь.
Главное: Ingress умеет только хост, путь и TLS, всё остальное живёт в аннотациях контроллера; новые кластеры строят на Gateway API.
Gateway API делит вход на слои по ролям, посмотрим на них.
Gateway API: три роли и три объекта
Gateway API сделан заново с учётом ошибок Ingress. Главная идея: вход делится на слои по ролям (role-oriented), и каждый слой принадлежит тому, кто им должен управлять.
Аналогия из аэропорта. Есть оператор аэропортов (он решает, какие вообще бывают терминалы и кто их обслуживает), есть управление терминалом (какие ворота открыты, какие проверки на входе), и есть авиакомпании (каждая объявляет свои рейсы: куда летит, в какие ворота). Авиакомпания не может открыть себе новые ворота или прописать рейс в чужой терминал без разрешения. Аналогия перестаёт работать в одном: у авиакомпаний нет общего журнала статуса, а у Gateway API он есть (ниже).
| Объект | Кто владеет | Что описывает |
|---|---|---|
| GatewayClass | провайдер платформы | какой контроллер обслуживает шлюзы (аналог StorageClass, класса хранилища из урока 5.5) |
| Gateway | администратор кластера | точка входа: порты, протоколы, имена хостов, сертификаты |
| HTTPRoute | команда приложения | маршруты: хост, путь, заголовки, веса, Service назначения |
Как они связаны:
flowchart TD
R["HTTPRoute notes<br>какие маршруты: notes.lab, путь / ведёт в Service notes:8080"] -->|"parentRefs: notes-gw"| G["Gateway notes-gw<br>какие двери: порт 80 (HTTP) и порт 443 (HTTPS), имя notes.lab"]
G -->|"gatewayClassName: eg"| C["GatewayClass eg<br>кто обслуживает: контроллер Envoy Gateway"]
R --> S["Service notes"] --> P["поды"]
Дальше: listener (слушатель) это одна «дверь» шлюза, комбинация порта, протокола и имени хоста. В нашем Gateway их два: http на порту 80 и https на порту 443.
parentRefs это поле в HTTPRoute, которым маршрут говорит: «я привязываюсь вот к этому Gateway». А allowedRoutes это поле в listener, которым Gateway решает, чьи маршруты он пустит: только из своего namespace (from: Same), из всех (All) или из выбранных. Поэтому разработчик не может тихо перехватить чужой домен: и он должен попроситься, и шлюз должен разрешить.
Возможности, ради которых раньше писали аннотации, теперь часть стандарта: веса трафика (канареечные релизы, когда новая версия получает сначала 10% запросов, тема 9), сопоставление по заголовкам, переписывание пути, редиректы. Есть и не-HTTP маршруты: GRPCRoute, TLSRoute, TCPRoute.
Статус вместо ошибок. Главное отличие в повседневной работе: контроллер пишет результат в поле status.conditions каждого объекта. Condition (условие) это запись вида «тип, значение True/False, причина, сообщение». Вот главные:
- у Gateway:
Accepted(контроллер принял объект) иProgrammed(прокси реально настроен); - у HTTPRoute:
Accepted(Gateway принял маршрут) иResolvedRefs(все ссылки, в первую очередь на Service, найдены).
Разобранный пример. Ты применил HTTPRoute, в parentRefs опечатка: notes-gateway вместо notes-gw. Команда kubectl apply отвечает «created»: формально объект корректен, Kubernetes не знает, что такого Gateway нет. Но в статусе маршрута не будет записи parents: контроллеру не к чему привязать маршрут, и писать условие ему некуда (Accepted: False с причиной NoMatchingParent бывает, когда Gateway есть, а listener или hostname не подошли). Смотри kubectl get httproute -o yaml: пустой status значит «Gateway не найден». Снаружи запрос получит 404 от Envoy: прокси жив, но маршрута для этого хоста у него нет. Диагностика Gateway API почти всегда сводится к чтению conditions.
Прикинь сам: кто в Gateway API решает, какие маршруты пустить на шлюз, и в каком поле?
Администратор через Gateway: поле allowedRoutes у listener. Команда приложения лишь привязывает HTTPRoute через parentRefs.
Осторожно: kubectl apply ответит «created» и на маршрут с опечаткой в parentRefs: ошибку нужно искать в статусе, а не в ответе команды.
Проверь понимание: кто в Gateway API решает, какие маршруты пустить на шлюз, и в каком поле?
Ответ
Администратор через Gateway: поле allowedRoutes у listener (from: Same, All или Selector). Команда приложения при этом только привязывает свой HTTPRoute через parentRefs. Если шлюз не разрешил, маршрут не принят (Accepted: False).
Главное: GatewayClass выбирает контроллер, Gateway описывает двери, HTTPRoute описывает маршруты; результат контроллер пишет в
status.conditions.
Раз маршруты стандартные, на них легко делать канареечный релиз.
Канареечный релиз одним полем: веса в HTTPRoute
Новую версию страшно выкатывать сразу всем: если в ней ошибка, пострадают все пользователи. Безопаснее отправить на неё сначала малую долю запросов и посмотреть, всё ли хорошо. Такой приём называют канареечным релизом (canary, по шахтёрским канарейкам, которые первыми чувствовали газ). В Ingress для этого писали аннотации, у каждого контроллера свои. В Gateway API это стандартное поле.
Новое блюдо в столовой. Сначала его дают одному столу из десяти. Если никто не жалуется, добавляют ещё, и так до всех. Аналогия перестаёт работать в том, что у посетителей есть выбор, а запросы на сервис распределяются случайно, и пользователь не выбирает, к какой версии попадёт.
В правиле HTTPRoute можно указать несколько Service в backendRefs, и каждому дать weight (вес). Envoy делит запросы пропорционально весам: доля = вес / сумма всех весов. Почему два Service: Service выбирает поды по меткам (урок 5.3), значит, у старой и новой версии должны быть разные метки (например, version: v1 и version: v2), и каждый сервис смотрит на свою группу. Для этого нужны два отдельных Service: один ведёт на поды старой версии, другой на поды новой.
Так выглядит фрагмент правила. Не применяй его, это только для чтения (сервиса notes-canary в нашем проекте нет):
rules:
- backendRefs:
- name: notes # сервис со стабильной версией
port: 8080
weight: 90 # 90 из 100 частей
- name: notes-canary # сервис с новой версией
port: 8080
weight: 10 # 10 из 100 частей
Сумма весов 90 + 10 = 100, значит канарейка получает 10 / 100 = 10% запросов. Веса 9 и 1 дали бы ту же долю, потому что важно только отношение. Вес 0 выключает бэкенд: так канарейку откатывают, не удаляя.
Прикинь сам: веса двух сервисов 3 и 1. Какая доля запросов уйдёт на второй?
1 / (3 + 1) = 25%. Доля считается от суммы весов, а не от числа 100.
Осторожно: не думай, что вес задаёт точное распределение на каждых десяти запросах. Нет: выбор случайный, и на малом числе запросов разброс большой (из 10 запросов на канарейку могут попасть и 0, и 3). Вторая путаница: вес относится к запросам, а не к пользователям. Один и тот же человек может получить то старую, то новую версию при двух нажатиях на «обновить».
Проверь понимание: веса двух сервисов 3 и 1. Какая доля запросов уйдёт на второй?
Ответ
1 / (3 + 1) = 25%. Доля считается от суммы весов, а не от числа 100.
Главное: доля запросов равна весу, делённому на сумму весов; вес 0 выключает бэкенд, не удаляя его.
Шлюз общий для нескольких команд, поэтому важно, кто что разрешает.
Кто что разрешает: namespace в Gateway API
В кластере живут несколько команд, каждая в своём namespace (урок 5.1). Вход общий, и нужно, чтобы одна команда не могла ни перехватить домен другой, ни забрать чужой сертификат. Без этого любой, у кого есть права в своём namespace, мог бы повлиять на чужой сайт.
Пропуска в бизнес-центре. Чтобы пройти в чужой офис, нужно и чтобы ты попросился, и чтобы хозяин офиса тебя впустил: одного желания мало. Аналогия перестаёт работать в одном: в кластере «охрана» это не человек, а проверка контроллера.
Разрешение нужно дважды, с обеих сторон:
- Маршрут в своём namespace просится на шлюз через
parentRefs, а шлюз вallowedRoutesрешает, пустить ли:from: Same(только из своего namespace),All(из любых) илиSelector(только из namespace с нужной меткой, напримерteam=shop; метка это пара «ключ=значение» на объекте, как стикер на папке из урока 5.2). Namespace напоминаю: это «отдельная комната» в кластере со своими именами объектов (урок 5.1). - Если объект ссылается на объект из чужого namespace (например, Gateway на Secret с сертификатом), владелец объекта должен создать в своём namespace ReferenceGrant: «разрешаю объектам такого-то вида из такого-то namespace ссылаться на мой такой-то Secret».
Gateway notes-gw в namespace notes, а сертификат в Secret в namespace certs. Без ReferenceGrant listener получает условие ResolvedRefs: False, а HTTPS не работает. Команда certs создаёт ReferenceGrant у себя, и условие становится True. Без разрешения ничего не ломается «громко»: Gateway создан, kubectl apply ответил «created», а порт 443 просто не заработал, поэтому смотреть надо статус.
Прикинь сам: у шлюза
allowedRoutes.namespaces.from: Same, а команда из другого namespace создала HTTPRoute с вернымparentRefs. Что увидит команда?
Маршрут создастся, но шлюз его не примет: Accepted: False, а снаружи будет 404. Чинить надо на стороне шлюза.
Осторожно: не думай, что ReferenceGrant нужен при любой ссылке. Нет: внутри одного namespace ссылки работают без него. И что allowedRoutes: All безопасен: это «пускай любого», оставляй только осознанно.
Проверь понимание: у шлюза
allowedRoutes.namespaces.from: Same, а команда приложения из другого namespace создала HTTPRoute с вернымparentRefs. Что увидит команда?
Ответ
Маршрут создастся, но шлюз его не примет: в статусе Accepted: False (причина вроде NotAllowedByListeners), а снаружи будет 404. Чинить надо на стороне шлюза: расширить allowedRoutes осознанно или перенести маршрут в namespace шлюза.
Главное: разрешение нужно с обеих сторон: маршрут просится через
parentRefs, шлюз пускает черезallowedRoutes; чужой Secret требует ReferenceGrant.
Gateway API лишь описание, а работу выполняет реализация: в курсе это Envoy Gateway.
Envoy Gateway и kind
Gateway API сам по себе это только набор описаний типов: CRD (Custom Resource Definition, «пользовательское определение ресурса»), то есть способ научить Kubernetes новым видам объектов. Никакого кода, который гоняет трафик, там нет. Его приносят реализации: Envoy Gateway, Traefik, Cilium, Istio, NGINX Gateway Fabric и другие. В курсе используем Envoy Gateway v1.9.2: Envoy это быстрый прокси, а Envoy Gateway это контроллер, который настраивает Envoy по объектам Gateway API. Манифест его релиза уже содержит CRD Gateway API v1.6.2, поэтому ставится он одним kubectl apply без Helm (менеджер пакетов для Kubernetes появится в уроке 5.9).
Что происходит после установки, по шагам:
- Контроллер
envoy-gatewayзапускается в отдельном namespaceenvoy-gateway-systemи следит за объектами Gateway API. - Ты создаёшь Gateway. Контроллер видит его и сам создаёт Deployment и Service с Envoy. Этих объектов нет в твоих файлах, ты их не пишешь.
- Envoy получает от контроллера настройки из твоих Gateway и HTTPRoute и начинает слушать порты.
- В облаке созданный Service получил бы тип LoadBalancer и внешний адрес. В kind балансировщика нет, поэтому через объект EnvoyProxy (настройки Envoy, это CRD Envoy Gateway) мы просим тип NodePort с фиксированными портами 30080 и 30443.
Почему фиксированные. В уроке 5.1 при создании кластера ты указал extraPortMappings: «порт 80 компьютера пробрасывать на порт 30080 узла, порт 443 на 30443». Проброс работает только на эти номера. Если оставить порты по умолчанию, Kubernetes выберет случайный NodePort, например 31844, и проброс упрётся в пустоту. Получается цепочка:
flowchart LR
A["notes.lab:80"] --> B["127.0.0.1:80<br>(компьютер)"] --> C["узел kind :30080<br>(NodePort)"] --> D["Envoy<br>(прокси)"] --> E["Service notes<br>(урок 5.3)"] --> F["под"]
Осторожно, путаница: «Gateway API не работает в kind». Работает, просто у kind нет облачного балансировщика, и его роль приходится сыграть вручную NodePort’ом и пробросом.
Как работает проброс, который ты настроил в уроке 5.1. Узлы kind это контейнеры Docker. Когда Docker публикует порт контейнера на компьютер (то же, что -p 80:30080 у docker run из урока 4.3), он строит правило DNAT (Destination NAT, «подмена адреса назначения»: ядро на лету меняет в пакете, куда он едет, как почтальон, который зачёркивает на конверте старый адрес и пишет новый): соединение на порт 80 компьютера уходит на порт 30080 внутри контейнера-узла. Пользоваться этим не надо, Docker делает всё сам, важно лишь знать, что проброс это просто «переадресация по номеру порта». А порт 30080 узла держит Kubernetes: NodePort сервиса Envoy. Вся цепочка стоит на двух совпадениях номеров, и обе половины лежат в разных местах: одна в конфиге kind (урок 5.1), другая в EnvoyProxy (этот урок). Если номера разъедутся, ничего не сломается видимо, просто порт перестанет вести куда надо.
Прикинь сам: почему для kind порты Service Envoy нужно закрепить, а в облаке не нужно?
kind пробросил на компьютер только порты узла 30080 и 30443, а случайный NodePort с ними не совпал бы. В облаке балансировщик получает свой адрес и порты 80 и 443.
Проверь понимание: почему для kind порты Service Envoy нужно закрепить, а в облаке не нужно?
Ответ
kind пробросил на компьютер только конкретные порты узла (30080 и 30443). NodePort по умолчанию случайный из диапазона 30000-32767 и не совпал бы с пробросом. В облаке балансировщик получает свой адрес и порты 80 и 443, и с номерами NodePort ничего согласовывать не надо.
Главное: контроллер сам создаёт Deployment и Service с Envoy; в kind цепочка держится на двух совпадениях номеров портов, в конфиге kind и в EnvoyProxy.
Посмотрим, как Envoy устроен внутри и откуда берёт адреса подов.
Что делает Envoy внутри: слушатель, маршрут, кластер
Чтобы понимать сообщения в логах и статусах, полезно представлять, из каких частей состоит настройка самого Envoy. У него три ключевых понятия, и они ложатся на объекты Gateway API один к одному:
- listener (слушатель): порт и протокол, на котором Envoy принимает соединения. Это твой listener
httpна порту 80 и listenerhttpsна 443; - route (маршрут): правило «если хост и путь такие, отправь вот туда». Это правила твоего HTTPRoute;
- cluster («кластер» в терминах Envoy, не путать с кластером Kubernetes): группа адресов, между которыми Envoy распределяет запросы. Для нас это адреса подов из EndpointSlice сервиса
notes.
Запрос проходит их по порядку: приняли соединение на listener, выбрали route по Host и пути, отправили в cluster на один из подов.
Интересная деталь: Envoy Gateway не переписывает файл настроек и не перезапускает Envoy при каждом изменении. Он передаёт настройки по сети на лету (протокол называется xDS), и изменения применяются за секунды без обрыва уже идущих запросов. Поэтому после kubectl apply маршрут начинает работать почти сразу, без ожидания перезапуска. Иногда нужно подождать 10-30 секунд, пока контроллер обработает изменение и дойдёт до Envoy: это нормально.
Самое важное следствие. Напомню из урока 5.3: у Service есть постоянный адрес ClusterIP, а EndpointSlice это актуальный список адресов готовых подов за ним, и обычно трафик к ClusterIP раскидывает по этим подам служба kube-proxy. Envoy эту цепочку обходит: он берёт адреса подов из EndpointSlice сам, напрямую, и не ходит ни через ClusterIP, ни через kube-proxy. Service в схеме нужен как источник списка подов и как «имя», на которое ссылается маршрут. Поэтому, если у сервиса нет готовых подов, Envoy честно скажет 503: ему просто некуда слать.
Прикинь сам: у сервиса
notesнет готовых подов. Что получит клиент через Envoy?
503: маршрут есть, но слать некуда. Envoy берёт адреса из EndpointSlice напрямую и ClusterIP с kube-proxy не использует.
Осторожно: «кластер» в терминах Envoy это группа адресов, а не кластер Kubernetes.
Главное: запрос проходит listener, route и cluster; настройки приходят в Envoy на лету по xDS без перезапуска.
Остаётся защитить вход шифрованием.
TLS на Gateway
TLS это шифрование, которое превращает HTTP в HTTPS (см. урок 2.6). Ему нужны две вещи: сертификат (публичная часть, «паспорт» сайта) и приватный ключ (секретная часть, он не должен попадать никому и никуда, включая git).
Как это устроено в Gateway API. Сертификат с ключом лежат в объекте Secret типа kubernetes.io/tls (Secret это объект для хранения секретных данных, подробно в уроке 5.6). В нём два поля: tls.crt (сертификат) и tls.key (ключ). Listener HTTPS ссылается на этот Secret через certificateRefs. Режим Terminate (завершать) означает: шифрованное соединение от браузера заканчивается на Envoy, Envoy его расшифровывает, а дальше до пода запрос идёт обычным HTTP внутри кластера. Поды при этом про сертификаты не знают, и это удобно: обновляешь сертификат в одном месте.
flowchart LR
B["браузер"] -->|"HTTPS, зашифровано"| E["Envoy<br>здесь TLS заканчивается"] -->|"HTTP, внутри кластера"| P["под notes"]
Ограничение: по умолчанию Secret должен лежать в том же namespace, что и Gateway. Ссылка на Secret из другого namespace требует явного разрешения: объекта ReferenceGrant («разрешение на ссылку»). Это защита: чужая команда не может подцепить твой сертификат. Такую поломку ты увидишь в конце урока.
Разобранный пример. Сертификат мы выпустим сами (самоподписанный, как в уроке 2.6): в него вписано имя notes.lab в поле SAN (Subject Alternative Name, «альтернативное имя субъекта»: список имён, для которых сертификат годится; браузеры и curl проверяют именно его). Центр сертификации (CA, certificate authority) это организация, которой браузеры заранее доверяют и которая подписью подтверждает: «этот сертификат действительно принадлежит этому сайту», как нотариус заверяет документ. Наш сертификат подписали мы сами, и браузер видит сертификат от неизвестного ему «нотариуса» и предупреждает. Шифрование работает, но доверия нет. Чтобы curl поверил, ему говорят: «доверяй именно этому файлу» (--cacert).
Долг проекта: Secret мы создаём командой из самоподписанного сертификата, и браузер ему не доверяет. В уроке 9.4 его заменит cert-manager, который выпускает настоящие сертификаты сам.
Прикинь сам: в режиме
Terminateподы получают HTTPS или HTTP? Где обновлять сертификат?
Поды получают обычный HTTP: шифрование заканчивается на Envoy. Сертификат обновляют в одном месте, в Secret, на который ссылается listener.
Осторожно: приватный ключ нельзя класть в git, а самоподписанный сертификат шифрует, но доверия не даёт.
Главное: сертификат и ключ лежат в Secret типа
kubernetes.io/tls, listener ссылается на него черезcertificateRefs; Secret по умолчанию в namespace шлюза.
Теперь разберём, как читать статус, где контроллер сообщает об ошибках.
Как читать status.conditions на настоящем примере
Раз ошибки живут в статусе, разберём, как он выглядит. Вот условия исправного HTTPRoute в выводе kubectl get httproute notes -n notes -o yaml (оставлена только нужная часть, время условное):
status:
parents:
- parentRef:
name: notes-gw # к какому Gateway относится этот блок статуса
controllerName: gateway.envoyproxy.io/gatewayclass-controller
conditions:
- type: Accepted
status: "True" # Gateway принял маршрут
reason: Accepted
message: Route is accepted
- type: ResolvedRefs
status: "True" # ссылки (на Service notes) найдены
reason: ResolvedRefs
message: Resolved all the Object references for the Route
По полям:
parents: статус пишется отдельно для каждого Gateway, к которому привязан маршрут. Один маршрут может быть привязан к нескольким шлюзам, и принят одним, но отклонён другим.type: что именно проверялось.status:"True"(в кавычках, это строка) значит «да»,"False"значит «нет».reason: короткая причина одним словом в стилеCamelCase. Именно её ищут в документации.message: причина человеческим языком.
При поломке меняется status и reason. Например, если Gateway найден, но listener или hostnames маршрута не подошли, ты увидишь в Accepted значение "False" и причину NoMatchingParent, а если в backendRefs сервис назван неверно, ResolvedRefs станет "False" с причиной BackendNotFound. Точные тексты сообщений зависят от версии контроллера, поэтому смотри в reason, а не в message.
В describe те же данные напечатаны в разделе Status: в виде текста, поэтому в проверках ниже используется sed -n '/Status:/,$p'. Разбор: sed -n не печатает ничего по умолчанию, а /Status:/,$p значит «начиная со строки, где встретилось Status:, и до конца вывода ($) напечатай (p)». Так ты отрезаешь длинное начало describe и видишь только статус.
Прикинь сам: в
Acceptedзначение"False"с причинойNoMatchingParent. Какое поле маршрута проверишь?
parentRefs (имя Gateway, sectionName) и hostnames: Gateway есть, но listener или hostname не подошли. Если же в status маршрута вообще нет parents, ищи опечатку в имени Gateway (например, notes-gateway вместо notes-gw). Причина в reason подсказывает поле быстрее, чем message.
Осторожно: значение status в YAML это строка в кавычках: "True", а не булево.
Главное: в
status.conditionsищиAcceptedиResolvedRefs; опирайся наreason, а не наmessage, тексты которого зависят от версии.
Последнее: как по коду ответа понять, на каком звене цепочки случилась поломка.
Как читать ошибки по цепочке: 404, 502, 503 и отказ
Через вход приходит несколько разных «не работает», и каждое указывает на своё звено. Это повторение урока 2.8 для кластера:
| Что видишь | Где ломается | С чего начать |
|---|---|---|
Connection refused, обрыв или пустой ответ на порту 80 |
до Envoy: нет проброса, NodePort не тот, Envoy не запущен | kubectl get svc -n envoy-gateway-system: должно быть 80:30080/TCP |
404 с телом от Envoy |
Envoy жив, но маршрут не подошёл | conditions HTTPRoute, hostnames, parentRefs |
503 |
маршрут есть, но за сервисом нет готовых подов (или соединение с подом сброшено до ответа) | kubectl get endpointslices, метки, targetPort (урок 5.3) |
502 |
Envoy дошёл до пода, но получил от него некорректный HTTP-ответ | логи приложения, port-forward на сервис |
ошибка TLS (certificate, SSL_ERROR_SYSCALL) |
сертификат не найден, не тот или не доверенный | conditions listener https, Secret |
Порядок один: идёшь сверху вниз по цепочке из схемы, пока не найдёшь первое звено, которое не отвечает.
Прикинь сам:
curlдаёт404от Envoy, аkubectl get podsпоказывает три готовых пода. Виноват ли сервисnotes?
Скорее нет: 404 значит, что запрос дошёл до прокси, но маршрута для хоста и пути нет. Смотри conditions HTTPRoute, parentRefs и hostnames.
Проверь понимание:
curlдаёт404от Envoy, аkubectl get podsпоказывает три готовых пода. Виноват ли сервисnotes?
Ответ
Скорее нет. 404 от Envoy значит, что запрос дошёл до прокси, но правила маршрута для этого хоста и пути не нашлось. Смотри conditions HTTPRoute (Accepted, ResolvedRefs), parentRefs и hostnames. Проблемы Service дали бы 503 или отказ соединения.
Главное: код ответа указывает звено: отказ соединения до Envoy,
404маршрут,503нет подов,502плохой ответ пода.
Теперь пора поставить вход в кластер самому.
Практика
Все команды выполняются на твоём компьютере с контекстом kind-notes (проверка: kubectl config current-context). Ключи -n (namespace) и -o (формат вывода) ты знаешь из уроков 5.2 и 5.3.
Задание 1. Ingress без контроллера
Цель: увидеть, что объект Ingress сам по себе ничего не делает, и запомнить формат для чтения чужих кластеров.
Предскажи: мы создадим Ingress для notes.lab в кластере, где нет контроллера. Что покажет колонка ADDRESS и что ответит curl?
Ответ
ADDRESS пустой, потому что никто не обработал объект. curl не получит ответа от приложения (на порту 80 узла kind ничего не слушает), хотя Ingress создан без ошибок.
Шаги:
-
Создай Ingress временным файлом. Разберём команды:
cat > файл <<'YAML' ... YAMLзаписывает всё между маркерами в файл (кавычки вокругYAMLотключают подстановки в тексте). Ключиcurl:-sS(тихо, но ошибки показывать),-m 3(ждать не больше 3 секунд),--resolve notes.lab:80:127.0.0.1(«считай, чтоnotes.labна порту 80 это 127.0.0.1»; так не нужно править/etc/hosts). Имяnotes.labв интернете не существует, поэтому такой приём нужен.cd ~/notes cat > /tmp/ingress-demo.yaml <<'YAML' apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: notes-demo namespace: notes spec: ingressClassName: nginx # такого контроллера у нас нет rules: - host: notes.lab http: paths: - path: / pathType: Prefix backend: service: name: notes port: number: 8080 YAML kubectl apply -f /tmp/ingress-demo.yaml kubectl get ingress -n notes curl -sS -m 3 --resolve notes.lab:80:127.0.0.1 http://notes.lab/healthz -
Удали демо: Ingress нужен был только для сравнения.
kubectl delete -f /tmp/ingress-demo.yaml && rm /tmp/ingress-demo.yaml
Что должно получиться:
ingress.networking.k8s.io/notes-demo created
NAME CLASS HOSTS ADDRESS PORTS AGE
notes-demo nginx notes.lab 80 5s
curl: (52) Empty reply from server
Текст ошибки curl зависит от системы и версии Docker: может быть (52) Empty reply from server, (56) Recv failure: Connection reset by peer или (7) Failed to connect. Главное, что ответа приложения нет.
Как читать вывод:
CLASS nginx: имя класса, которое мы написали, но контроллера с таким классом нет.HOSTS notes.lab,PORTS 80: правило принято как данные.ADDRESSпустой: адрес выдаёт контроллер, а его нет.- Ошибка
curl: на порту 80 компьютера работает только проброс kind, за ним пусто.
Объясни себе:
- Почему объект создался без ошибок, если контроллера нет?
Типичные ошибки:
Error from server (NotFound): namespaces "notes" not found: не создан namespace из урока 5.1:kubectl apply -f k8s/base/00-namespace.yaml.curlне отвечает и после установки контроллера, хотя Ingress создан: проверь, что имя вingressClassNameсовпадает с классом реального контроллера (kubectl get ingressclass). Ingress не проверяет и то, что Servicenotesсуществует: создан ли он, убедись сам (урок 5.3).
Задание 2. Ставим Envoy Gateway
Цель: получить в кластере контроллер и CRD Gateway API.
Предскажи: какие новые типы объектов появятся после установки и в каком namespace запустится контроллер?
Ответ
Появятся CRD gateways, gatewayclasses, httproutes и другие из gateway.networking.k8s.io, а также envoyproxies из gateway.envoyproxy.io. Контроллер запустится в namespace envoy-gateway-system.
Шаги:
-
Установи манифест релиза.
kubectl apply -f <адрес>умеет читать файл по ссылке. Ключ--server-side(«применить на стороне сервера») нужен: обычныйapplyзаписывает копию манифеста в аннотацию объекта, а CRD слишком велики для аннотации (лимит 262144 байта).kubectl waitждёт, пока условие выполнится:--for=condition=Availableзначит «контроллер запущен и доступен»,--timeout=5mдаёт на это пять минут.kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/v1.9.2/install.yaml kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available -
Проверь результат.
grep -cсчитает число совпавших строк.kubectl get pods -n envoy-gateway-system kubectl get crd | grep -c 'gateway.networking.k8s.io'
Что должно получиться:
NAME READY STATUS RESTARTS AGE
envoy-gateway-7b9c8d6f5-x2k4q 1/1 Running 0 40s
Вторая команда напечатает число больше нуля (сколько CRD Gateway API установлено). Имя пода и хэш в нём у тебя будут другими.
Как читать вывод: READY 1/1 значит «один контейнер из одного запущен», STATUS Running значит «работает». Если Pending или ContainerCreating, образ ещё скачивается.
Объясни себе:
- Почему CRD ставятся вместе с контроллером, а не лежат в самом Kubernetes?
Типичные ошибки:
Too long: must have at most 262144 bytes: применили без--server-side: повтори с--server-side.error: timed out waiting for the condition: слабый диск или сеть тянут образ слишком долго:kubectl describe pod -n envoy-gateway-system, смотри раздел Events.
Если у тебя 8 ГБ
Envoy Gateway и три пода notes вместе с kind укладываются в 8 ГБ, но впритык. Снизь нагрузку: оставь у Deployment notes две реплики (kubectl scale deploy/notes -n notes --replicas=2) и закрой браузер и IDE на время урока. Ничего другого ставить не нужно, Envoy запускается в одном экземпляре. В конце урока верни три реплики.
Задание 3. Gateway и HTTPRoute
Цель: описать вход манифестами и получить ответ notes через http://notes.lab.
Предскажи: что будет, если применить только HTTPRoute без Gateway? Что покажет его статус?
Ответ
Объект создастся, но Accepted не станет True: маршруту не к кому привязаться (parentRefs указывает на несуществующий Gateway). Трафика не будет. Ошибки от API не будет, ошибка живёт только в статусе.
Шаги:
-
Файл
~/notes/k8s/base/30-envoyproxy.yaml: настройки Envoy (NodePort с фиксированными портами). Блокpatchговорит: «возьми Service, который сам создашь, и наложи на него эти поля» (StrategicMerge это способ слияния YAML, при котором списки сопоставляются поname).apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: notes-proxy namespace: envoy-gateway-system spec: provider: type: Kubernetes kubernetes: envoyService: type: NodePort # в kind нет облачного балансировщика patch: type: StrategicMerge value: spec: ports: - name: http-80 port: 80 nodePort: 30080 # kind пробросил его на порт 80 компьютера - name: https-443 port: 443 nodePort: 30443 # и этот на 443 -
Файл
~/notes/k8s/base/31-gateway.yaml: класс и точка входа с двумя listeners. Разделитель---позволяет положить два объекта в один файл.apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: # какой контроллер обслуживает этот класс (имя закреплено в Envoy Gateway) controllerName: gateway.envoyproxy.io/gatewayclass-controller # где взять настройки Envoy: наш объект EnvoyProxy из файла 30 parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy name: notes-proxy namespace: envoy-gateway-system --- apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: notes-gw namespace: notes spec: gatewayClassName: eg # обслуживает класс eg listeners: - name: http # дверь 1: обычный HTTP protocol: HTTP port: 80 hostname: notes.lab allowedRoutes: namespaces: from: Same # пускаем маршруты только из namespace notes - name: https # дверь 2: HTTPS protocol: HTTPS port: 443 hostname: notes.lab tls: mode: Terminate # TLS заканчивается на Envoy certificateRefs: - kind: Secret name: notes-tls # Secret создадим в задании 4 allowedRoutes: namespaces: from: Same -
Файл
~/notes/k8s/base/32-httproute.yaml: маршрут команды приложения.apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: notes namespace: notes spec: parentRefs: - name: notes-gw # к какому Gateway привязываемся hostnames: - notes.lab # для какого имени сайта маршрут rules: - matches: - path: type: PathPrefix # все пути, начинающиеся с / value: / backendRefs: - name: notes # Service из урока 5.3 port: 8080 -
Применяй и жди. Listener HTTPS пока будет с ошибкой: Secret ещё нет, это нормально. Разбор:
-fможно повторять, чтобы применить сразу несколько файлов.-o jsonpath='...'вытаскивает из объекта только нужные поля:{.status.parents[0].conditions[*].type}значит «типы всех условий первого родителя».curl -w '%{http_code}\n'печатает только код ответа.cd ~/notes kubectl apply -f k8s/base/30-envoyproxy.yaml -f k8s/base/31-gateway.yaml -f k8s/base/32-httproute.yaml kubectl get gatewayclass eg kubectl get httproute notes -n notes -o jsonpath='{.status.parents[0].conditions[*].type}{"\n"}' curl -s -o /dev/null -w '%{http_code}\n' --resolve notes.lab:80:127.0.0.1 http://notes.lab/healthz
Что должно получиться:
envoyproxy.gateway.envoyproxy.io/notes-proxy created
gatewayclass.gateway.networking.k8s.io/eg created
gateway.gateway.networking.k8s.io/notes-gw created
httproute.gateway.networking.k8s.io/notes created
NAME CONTROLLER ACCEPTED AGE
eg gateway.envoyproxy.io/gatewayclass-controller True 20s
Accepted ResolvedRefs
200
Если curl сразу вернул 000, подожди 20-30 секунд: Envoy стартует после создания Gateway.
Как читать вывод:
ACCEPTED Trueу GatewayClass: контроллер признал класс своим.Accepted ResolvedRefs: у маршрута есть оба условия (типы). Значения смотри черезdescribe: должны быть True.200: запрос прошёл всю цепочку.000значит, чтоcurlне смог соединиться.
Объясни себе:
- Кто создал Service и Deployment с Envoy, если их нет в твоих манифестах?
- Что означает
from: Sameи что случится с HTTPRoute из другого namespace?
Типичные ошибки:
no matches for kind "Gateway" in version "gateway.networking.k8s.io/v1": CRD не установлены: вернись к заданию 2.curl: (7) Failed to connect to notes.lab port 80: Envoy ещё не запущен или порты не сошлись:kubectl get svc -n envoy-gateway-system, ищи80:30080/TCP.- Ответ
404с пустым телом: Envoy жив, но маршрут не подошёл: проверьhostnamesи заголовокHost(при--resolveон верный).
Задание 4. TLS и шаг проекта
Цель: включить HTTPS на Gateway и зафиксировать вход в репозитории проекта. Это шаг сквозного проекта «Заметки».
Предскажи: после создания Secret notes-tls какие условия у listener https станут True и почему curl без --cacert всё равно откажет?
Ответ
Станут True Programmed, Accepted и ResolvedRefs (ссылка на Secret найдена). curl откажет с SSL certificate problem: сертификат самоподписанный, у системы нет причин ему доверять. Шифрование при этом работает.
Шаги:
-
Выпусти самоподписанный сертификат с SAN
notes.lab(как в уроке 2.6) во временный каталог и создай из него Secret в namespacenotes. Разборopenssl req:-x509выпустить сразу сертификат, а не запрос;-newkey rsa:2048создать новый ключ;-nodesне шифровать ключ паролем;-days 365срок;-keyoutи-outкуда писать;-subjимя владельца;-addextдобавить SAN.mktemp -dсоздаёт временный каталог. Копия сертификата/tmp/notes-lab.crtнужна для проверки вcurl, приватный ключ удаляется вместе с каталогом.T=$(mktemp -d) openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -keyout "$T/tls.key" -out "$T/tls.crt" \ -subj "/CN=notes.lab" -addext "subjectAltName=DNS:notes.lab" kubectl create secret tls notes-tls -n notes --cert="$T/tls.crt" --key="$T/tls.key" cp "$T/tls.crt" /tmp/notes-lab.crt rm -rf "$T" -
Проверь листенеры и запрос по HTTPS. Флаг
--cacertговоритcurlдоверять именно этому сертификату. Длинная командаkubectl get gatewayперебирает listeners (range) и печатает для каждого имя и статус условияProgrammed.kubectl get gateway notes-gw -n notes -o jsonpath='{range .status.listeners[*]}{.name}{" "}{.conditions[?(@.type=="Programmed")].status}{"\n"}{end}' curl -s -o /dev/null -w '%{http_code}\n' --cacert /tmp/notes-lab.crt --resolve notes.lab:443:127.0.0.1 https://notes.lab/healthz -
Зафиксируй в git. Secret в репозиторий не кладём: это известный долг (закроется в уроке 9.4).
cd ~/notes git add k8s/base/30-envoyproxy.yaml k8s/base/31-gateway.yaml k8s/base/32-httproute.yaml git commit -m "k8s: вход через Envoy Gateway (Gateway API)" -
Если в 8 ГБ-режиме ты уменьшал реплики, верни три:
kubectl scale deploy/notes -n notes --replicas=3.
Манифесты остаются в твоём репозитории ~/notes/k8s/base.
Что должно получиться:
secret/notes-tls created
http True
https True
200
Как читать вывод: после строки о создании Secret идут две строки имя статус: имя listener и статус его условия Programmed (True значит, что он настроен). Последняя строка это код ответа по HTTPS. Команда openssl при выпуске сертификата может напечатать служебные строки про ключ, они не важны.
Объясни себе:
- Где именно в цепочке заканчивается TLS и что идёт дальше до пода?
- Почему приватный ключ сертификата не должен попасть в git, и как долг закрывается позже?
Типичные ошибки:
curl: (60) SSL certificate problem: self-signed certificate: не передан--cacert: добавь его или временно-k(не проверять сертификат).Error from server (AlreadyExists): secrets "notes-tls" already exists: Secret уже есть:kubectl delete secret notes-tls -n notesи создай заново.curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL: у листенера нет сертификата (Secret не найден): смотри conditions Gateway.
Нейросеть хорошо читает
status.conditions, но предлагает «пересоздать Gateway». Сначала открой поле, которое она назвала (parentRefs,hostnames,backendRefs), в своём манифесте.
Сломай и почини
Скачай скрипт поломки и запусти один из сценариев (номер от 1 до 3, либо random). Скрипт не читай: причину ищи по симптомам.
curl -fsSL -o /tmp/break-5.4.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/5.4/break.sh
bash /tmp/break-5.4.sh random
Вернуть исправное состояние: bash /tmp/break-5.4.sh fix (безопасно запускать повторно).
Симптом
Вход через notes.lab перестал работать так, как в задании 4. Что именно не так, зависит от сценария: то 404 или пустой ответ, то отказ соединения, то ошибка TLS. Приложение и Service при этом в порядке: kubectl port-forward svc/notes 8080:8080 -n notes отдаёт /healthz.
Гипотезы
Цепочка сверху вниз, как в уроке 2.8: имя, порт компьютера, NodePort, Envoy, Gateway Programmed, HTTPRoute Accepted и ResolvedRefs, endpoints Service. Какие звенья ломаются тихо, без ошибки apply?
Проверки
kubectl get gateway,httproute -n notes
kubectl describe httproute notes -n notes | sed -n '/Status:/,$p'
kubectl describe gateway notes-gw -n notes | sed -n '/Status:/,$p'
kubectl get svc -n envoy-gateway-system
kubectl get secret -A | grep notes-tls
kubectl get endpointslices -n notes -l kubernetes.io/service-name=notes
Здесь sed -n '/Status:/,$p' печатает только часть вывода от строки со словом Status: до конца, где лежат conditions. grep notes-tls в списке всех Secret всех namespace показывает, где лежит нужный.
Исправление
Разбор трёх сценариев
1. HTTPRoute без Accepted. В kubectl get httproute -o yaml у маршрута пустой status (нет parents; при несовпавшем listener или hostname было бы Accepted: False с причиной NoMatchingParent): в parentRefs опечатка в имени Gateway (например, notes-gateway). API такое принимает молча. Исправление: parentRefs[0].name: notes-gw и kubectl apply -f k8s/base/32-httproute.yaml. Симптом снаружи: 404 от Envoy.
2. Gateway без доступа снаружи. Gateway Programmed: True, HTTPRoute Accepted, а с компьютера curl получает отказ или пустой ответ. Проверка kubectl get svc -n envoy-gateway-system: у Service Envoy NodePort не 30080 и 30443 (например, 80:31080/TCP): в EnvoyProxy стоят другие номера (в реальной жизни это бывает и когда nodePort вовсе не указан: тогда номер случайный). Исправление: вернуть nodePort: 30080 и 30443 в 30-envoyproxy.yaml, применить, убедиться в 80:30080/TCP. Ловушка: снаружи это выглядит как «Envoy упал», а Envoy в порядке, просто не совпали порты с пробросом kind.
3. TLS Secret в неверном namespace. Listener https в статусе: ResolvedRefs: False, причина InvalidCertificateRef или RefNotPermitted, а curl https даёт SSL_ERROR_SYSCALL или unexpected eof. Secret notes-tls лежит в default, а Gateway в notes и ссылается на тот же namespace. Исправление: создать Secret в notes (команда из задания 4, -n notes) и удалить лишний. HTTP по порту 80 при этом работает, что подсказывает: проблема именно в листенере HTTPS.
Выводы для дежурства: у Gateway API ошибки не в выводе apply, а в status.conditions, поэтому первым делом читай describe Gateway и HTTPRoute.
ИИ в помощь
Нейросеть быстро объясняет поля Gateway API и читает статусы, но она не видит твой кластер, а про версии Gateway API и Envoy Gateway может помнить старое. Общие правила: ИИ-помощник.
Задача: разобрать status.conditions.
Я учу Gateway API. Вот вывод kubectl get httproute notes -n notes -o yaml (блок status):
<вставь вывод>
Объясни по полям, что значат Accepted и ResolvedRefs, какая причина (reason) и какое поле манифеста
надо проверить. Дай не больше трёх проверок от самой дешёвой.
Проверь ответ: сверь с таблицей кодов из теории и открой поле, которое назвала нейросеть, в своём манифесте. Типичная ошибка: она предлагает «пересоздать Gateway», хотя причина в одной строке parentRefs.
Задача: найти звено цепочки по коду ответа.
Через вход в кластер curl отвечает <вставь код ответа и тело>. Цепочка: порт компьютера, NodePort узла,
Envoy, маршрут HTTPRoute, Service, поды. Скажи, на каком звене искать причину и какие команды kubectl
выполнить по порядку.
Проверь ответ: сопоставь с таблицей «404, 502, 503 и отказ». Типичная ошибка: нейросеть лечит приложение при 404, хотя это ответ самого Envoy о том, что маршрута нет.
Задача: перевести Ingress в HTTPRoute.
Вот мой Ingress: <вставь манифест>. Перепиши его в Gateway API: Gateway с listener на порту 80
и HTTPRoute. Имена и сервисы не меняй. Отдельно перечисли аннотации, которые не переносятся.
Проверь ответ: примени с kubectl apply --dry-run=server -f и прочитай conditions после применения. Типичная ошибка: нейросеть оставляет ingressClassName или аннотации ingress-nginx, а в parentRefs пишет имя шлюза, которого нет.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Reverse proxy (обратный прокси) | Программа-посредник, которая принимает запросы снаружи и передаёт их нужному серверу внутри |
| Контроллер (controller) | Программа в кластере, которая читает объекты и настраивает реальную работу |
| Ingress | Старый объект «хост и путь ведут в Service», без контроллера бесполезен |
IngressClass, ingressClassName |
Имя контроллера, которому адресован Ingress |
| Аннотация (annotation) | Поле-заметка в объекте, которое читает конкретный контроллер, у каждого свои |
| Gateway API | Новый стандарт описания входа: GatewayClass, Gateway, HTTPRoute |
| GatewayClass | Какой контроллер обслуживает шлюзы |
| Gateway | Точка входа: порты, имена хостов, сертификаты |
| Listener | Одна «дверь» шлюза: порт плюс протокол плюс имя хоста |
| HTTPRoute | Маршрут: для какого хоста и пути в какой Service отправлять запрос |
parentRefs |
Поле маршрута: к какому Gateway он привязывается |
allowedRoutes |
Поле listener: чьи маршруты пускать |
| Condition (условие) | Запись статуса «тип, True/False, причина»: Accepted, Programmed, ResolvedRefs |
| CRD | Способ научить Kubernetes новому типу объектов |
| Envoy, Envoy Gateway | Прокси и контроллер, который настраивает его по объектам Gateway API |
| EnvoyProxy | Объект с настройками Envoy (например, тип Service) |
| NodePort | Порт (30000-32767) на каждом узле, через который сервис виден снаружи |
| TLS termination | Расшифровка HTTPS на входе, дальше до пода идёт обычный HTTP |
Secret типа kubernetes.io/tls |
Объект с сертификатом (tls.crt) и ключом (tls.key) |
| ReferenceGrant | Разрешение ссылаться на объект из другого namespace |
| Канареечный релиз (canary) | Новая версия получает малую долю запросов, пока её проверяют |
weight |
Вес бэкенда в HTTPRoute: доля запросов = вес / сумма весов |
| Маршрутизация | Решение, куда отправить запрос по имени сайта и пути |
| SAN | Список имён внутри сертификата, для которых он годится |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Чем Ingress отличается от Service типа LoadBalancer?
Ответ
LoadBalancer это один внешний адрес на один Service, часто платный балансировщик облака. Ingress (или Gateway) это один вход на многих сервисов: по хосту и пути он раздаёт запросы разным Service и завершает TLS. Один балансировщик и много маршрутов дешевле и удобнее.
Что хотят услышать: уровень TCP против уровня HTTP (L4 и L7), экономия внешних адресов, TLS на входе, нужен контроллер.
Красный флаг: «это одно и то же, просто два способа».
2. [junior] [часто] Что такое Gateway API и чем он лучше Ingress?
Ответ
Это преемник Ingress: три объекта (GatewayClass, Gateway, HTTPRoute) с разделением ролей. Администратор владеет входом и сертификатами, команда владеет маршрутами. Веса, заголовки, редиректы и переписывание путей входят в стандарт, а не в аннотации, поэтому конфиги переносятся между реализациями.
Что хотят услышать: разделение ролей, переносимость, не только HTTP, статус в conditions.
Красный флаг: «просто новый Ingress с другим синтаксисом».
3. [junior] Ты применил Ingress, kubectl get ingress показывает объект, а сайт не открывается и ADDRESS пуст. Что делаешь?
Ответ
Проверяю, что в кластере вообще есть контроллер и подходит ли ingressClassName (kubectl get ingressclass, поды контроллера). Пустой ADDRESS почти всегда значит, что объект никто не обработал. Потом смотрю логи контроллера и события объекта (describe ingress).
Что хотят услышать: объект не равен реализации, IngressClass, describe ingress, логи контроллера.
Красный флаг: пересоздавать Ingress «на всякий случай» или перезапускать поды приложения.
4. [junior] [на скорость] Что случилось с ingress-nginx и что ты выберешь для нового кластера?
Ответ
Проект ingress-nginx выведен из поддержки (архивирован в 2026), новые уязвимости не закрываются. Для нового кластера я беру Gateway API с реализацией, которую поддерживает мой провайдер или команда: Envoy Gateway, Cilium, Traefik, NGINX Gateway Fabric. Старые кластеры планирую мигрировать поэтапно.
Что хотят услышать: знание факта, Gateway API как вектор, план миграции, а не паника.
Красный флаг: «ставлю ingress-nginx, он самый популярный».
5. [middle] Прод отвечает 502 через Gateway, что делаешь?
Ответ
Иду по цепочке. Сначала kubectl get httproute,gateway и conditions: принят ли маршрут, найден ли backend. Затем endpoints сервиса: есть ли готовые поды, не упали ли пробы. Потом логи пода Envoy (доступ и ошибки до пода) и port-forward прямо на Service, чтобы понять, отвечает ли само приложение.
Что хотят услышать: порядок сверху вниз, endpoints, readiness (проверка готовности пода), различие 502 (плохой ответ или обрыв от пода), 503 (нет здоровых подов) и 404 (не подошёл маршрут).
Красный флаг: сразу рестартить Envoy или приложение без диагностики.
6. [middle] Ты применил HTTPRoute, apply прошёл без ошибок, а сайт отдаёт 404. Где искать?
Ответ
Ошибки Gateway API живут в статусе. Смотрю describe httproute: Accepted и ResolvedRefs. Типичные причины: неверное имя в parentRefs (тогда в статусе маршрута нет parents), hostname не пересекается с listener, маршрут из чужого namespace не разрешён в allowedRoutes. Проверяю и заголовок Host в запросе.
Что хотят услышать: conditions, parentRefs, allowedRoutes, hostnames, заголовок Host.
Красный флаг: «в логах apply ошибок нет, значит проблема в приложении».
7. [middle] [на скорость] Как в Gateway API сделать канареечный релиз 10% трафика на новую версию?
Ответ
В HTTPRoute у правила два backendRefs на разные Service (стабильный и канарейка) с weight: 90 и weight: 10. Веса это часть стандарта, аннотации не нужны. Меняю веса шагами и смотрю метрики ошибок. Автоматизирует это Argo Rollouts (тема 9).
Что хотят услышать: weight, два Service, метрики как условие продолжения, откат весом 0.
Красный флаг: «нужно два Ingress с аннотацией canary»: это специфика одного контроллера.
8. [middle] HTTPS на Gateway не поднимается: ошибка про сертификат. Что проверишь?
Ответ
describe gateway и условие ResolvedRefs у listener. Проверяю: Secret существует, тип kubernetes.io/tls, ключи tls.crt и tls.key, Secret лежит в том же namespace, что и Gateway (иначе нужен ReferenceGrant). Затем SAN сертификата и срок действия через openssl x509 -noout -text.
Что хотят услышать: namespace, тип Secret, ReferenceGrant, SAN, срок действия.
Красный флаг: отключить TLS «чтобы заработало».
9. [middle] В kind Gateway создан, но снаружи недоступен. Прод в облаке работает по тому же манифесту. Почему?
Ответ
В облаке Service Envoy получает LoadBalancer и внешний адрес. В kind балансировщика нет, я явно задаю NodePort и проброс extraPortMappings на компьютер. Ошибка обычно в несовпадении портов: NodePort случайный или другой, а проброс ждёт 30080. Проверяю kubectl get svc в namespace Envoy и docker ps для проброса.
Что хотят услышать: LoadBalancer против NodePort, extraPortMappings, порты, MetalLB (компонент, выдающий адреса для LoadBalancer вне облака) как аналог.
Красный флаг: «Gateway API не работает на локальных кластерах».
10. [middle] Как перенести существующий Ingress на Gateway API без простоя?
Ответ
Поднимаю Gateway рядом со старым входом и переношу маршруты в HTTPRoute (есть утилита ingress2gateway, но результат проверяю руками, аннотации не переносятся). Проверяю новый вход через curl --resolve на его адрес, затем переключаю DNS или вес на новый адрес с малым TTL (временем жизни DNS-записи в кэше). Старый Ingress убираю после наблюдения за метриками.
Что хотят услышать: параллельный вход, проверка до переключения, DNS с низким TTL, аннотации переписываются вручную.
Красный флаг: удалить Ingress и применить HTTPRoute в один заход.
11. [junior] [на скорость] Что такое Ingress Controller и почему объекта Ingress самого по себе недостаточно?
Ответ
Ingress - это только описание правил: какой host и путь отправить в какой Service. Сам трафик никто не обрабатывает, пока в кластере не запущен контроллер (например, на базе nginx, Envoy или Traefik), который читает эти объекты и настраивает свой прокси. Смотрю kubectl get pods -A на наличие контроллера и kubectl get ingressclass. В Ingress указываю ingressClassName, чтобы правило подхватил нужный контроллер.
Что хотят услышать: Ingress = правила, контроллер = исполнитель, IngressClass, контроллер ставится отдельно, внешний адрес появляется от контроллера.
Красный флаг: «Ingress сам принимает трафик», «поставил манифест, значит работает».
12. [junior] Как Ingress или HTTPRoute направляет запросы на разные сервисы: по host и по path?
Ответ
Правило выбирается по заголовку Host и по пути запроса. Например, api.example.com идёт в Service api, а example.com/static в Service static. В Ingress у пути есть pathType: Exact совпадает только с точным путём, Prefix совпадает по сегментам пути (/app подойдёт для /app/x). В HTTPRoute тот же смысл задают через hostnames и matches с path. Проверяю результат через curl -H 'Host: api.example.com' http://<адрес>/.
Что хотят услышать: host и path routing, Exact и Prefix, проверка через заголовок Host, один балансировщик на много сервисов.
Красный флаг: Путать Prefix с подстрокой («/app подойдёт для /application»), не знать, что Host учитывается.
13. [middle] Как на входе в кластер сделать редирект HTTP на HTTPS и чем отличается TLS termination от passthrough?
Ответ
Редирект настраиваю на HTTP-слушателе: в Gateway API это фильтр RequestRedirect в HTTPRoute со scheme: https, в Ingress зависит от контроллера (аннотация или настройка). При termination TLS заканчивается на входе: шлюз держит сертификат, расшифровывает и дальше идёт в под по HTTP или по новому TLS. При passthrough шлюз не расшифровывает, а пропускает зашифрованный поток к поду, поэтому не видит путь и заголовки и не может маршрутизировать по ним. Обычно выбираю termination: сертификаты в одном месте, работает L7-маршрутизация.
Что хотят услышать: RequestRedirect или аннотация контроллера, termination против passthrough, при passthrough нет L7-маршрутизации, где лежит сертификат.
Красный флаг: Думать, что passthrough тоже умеет маршрутизировать по пути.
Проверено на версиях
Кластер в этой редакции не запускался. Манифесты Gateway API и Envoy проверены статически (kubeconform -strict -ignore-missing-schemas: у CRD Gateway API и Envoy Gateway схем нет, поэтому они пропускаются и синтаксис проверен только как YAML). Вывод команд оставлен по документации и знанию версий, имена, хэши и время условные. Скрипт поломок проверен shellcheck, но не запускался.
- kind: v0.33.0
- Kubernetes: 1.37.1
- kubectl: 1.37.1
- Gateway API: v1.6.2 (CRD входят в манифест Envoy Gateway)
- Envoy Gateway: v1.9.2 (
kubectl apply --server-side, без Helm) - OpenSSL из Ubuntu 26.04 LTS и 24.04
- ingress-nginx: выведен из поддержки, в курсе не используется, проверь актуальный статус на странице проекта
Итог урока: ты умеешь
- умею объяснить, чем общий вход лучше NodePort и LoadBalancer на каждый сервис
- умею объяснить, почему объект Ingress без контроллера не пропускает трафик
- умею назвать причины, по которым Gateway API заменяет Ingress, и роли GatewayClass, Gateway, HTTPRoute
- умею установить Envoy Gateway v1.9.2 через
kubectl apply --server-side - умею описать вход в кластер манифестами Gateway и HTTPRoute для
notes.lab - умею выпустить TLS Secret командой и подключить его к listener HTTPS
- умею читать
status.conditionsGateway и HTTPRoute и находить причину 404 и отказа соединения - умею пройти цепочку «хост -> NodePort -> Envoy -> Service -> под» при диагностике
Дальше: Урок 5.5: Хранилище и StatefulSet: PostgreSQL в кластере
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.