Kubernetes для самых маленьких. Часть 18: Как работают Ingress и Gateway API?
Все части серии «Kubernetes для самых маленьких»В прошлой части MetalLB выдал сервису типа LoadBalancer адрес 192.168.10.200 из пула, и до пода стало можно достучаться снаружи кластера. Но один адрес — это один сервис: если у нас десять веб-приложений, понадобится десять адресов, десять записей в DNS и десять мест, где настраивается TLS. Сегодня поставим перед приложениями один обратный прокси, который принимает весь HTTP и HTTPS на 192.168.10.200 и раскладывает запросы по сервисам по имени сайта и пути. В Kubernetes для его настройки есть два API — старый Ingress и новый Gateway API, — и оба мы разберём на одном контроллере, Traefik 3.7. Сервисы и kube-proxy из части 15 и DNS из части 16 пригодятся по ходу дела.
Неймспейс demo с Deployment hello из трёх подов собран заново по части 11, сервис hello — по части 15, MetalLB 0.16.1 в режиме L2 с пулом 192.168.10.200–220 — по части 17.
Зачем маршрутизация на L7
Сервис типа LoadBalancer работает на четвёртом уровне: MetalLB объявляет адрес, kube-proxy переписывает адрес назначения пакета на адрес пода, и на этом всё. Ни имя сайта, ни путь запроса, ни заголовки на этом уровне не видны — только адреса и порты. Отсюда ограничения:
- один адрес на сервис: на
192.168.10.200:443может слушать только один сервис, и два сайта на одном адресе не разделить; - TLS в каждом приложении: каждый под должен сам держать сертификат и ключ, сам их обновлять и сам выбирать шифры;
- никакой общей политики: перенаправление с HTTP на HTTPS, заголовки безопасности, ограничение частоты запросов, журнал обращений — каждое приложение делает это по-своему или не делает вовсе.
HTTP — протокол прикладного уровня поверх TCP: клиент шлёт запрос (метод, путь, заголовки, в том числе Host с именем сайта), сервер отвечает кодом состояния, заголовками и телом; в HTTP/2 те же запросы идут двоичными кадрами, по многу одновременно в одном соединении. Обратный прокси — сервер, который принимает соединение клиента, читает запрос и сам открывает соединение к нужному бэкенду; поскольку он видит запрос целиком, он может выбрать бэкенд по Host и пути, снять TLS и добавить заголовки. Подробно о прокси — в части 20 сетевой серии.
Для HTTPS прокси нужно ещё до расшифровки понять, какой сертификат показать. Для этого есть SNI (Server Name Indication) — расширение TLS, в котором клиент открытым текстом называет имя сервера в первом сообщении рукопожатия (ClientHello). По SNI прокси выбирает сертификат, после рукопожатия читает уже расшифрованный запрос и маршрутизирует по Host и пути.
Так получается схема, которую мы построим:
ноутбук администратора 192.168.10.50
│ https://hello.apps.example.com/ (DNS: *.apps.example.com → 192.168.10.200)
▼
192.168.10.200:443 MetalLB (L2): узел отвечает на ARP за адрес
│
▼
kube-proxy, nftables: DNAT на под Traefik, порт 8443
│
▼
┌─────────────────────── под Traefik ───────────────────────┐
│ TLS по SNI: сертификат *.apps.example.com │
│ маршрут: Host = hello.apps.example.com, путь / → hello │
│ middleware: заголовки, rate limit, журнал доступа │
└───────────────────────────┬───────────────────────────────┘
│ HTTP на адрес пода из EndpointSlice
▼
под hello (172.20.x.x:8080) в неймспейсе demo
Все сайты *.apps.example.com указывают в DNS на один адрес; различает их уже прокси. LoadBalancer нужен ровно один — у самого прокси.
Ingress: объект
Ingress — объект API группы networking.k8s.io/v1, который описывает правила: «запросы с таким Host и таким путём отправлять в такой Service на такой порт». Сам по себе Ingress ничего не делает — в кластере нет встроенного компонента, который бы его читал. Это делает ingress-контроллер, о нём ниже.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
namespace: demo
spec:
ingressClassName: traefik
tls:
- hosts:
- hello-ingress.apps.example.com
secretName: hello-ingress-tls
rules:
- host: hello-ingress.apps.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
- path: /healthz
pathType: Exact
backend:
service:
name: hello
port:
name: http
rules — список правил, у каждого необязательный host и список путей. host бывает точным (hello.apps.example.com) или с маской в первой метке (*.apps.example.com); маска покрывает ровно одну метку, то есть a.b.apps.example.com и сам apps.example.com под неё не попадают. Правило без host срабатывает для любого имени.
pathType обязателен и бывает трёх видов:
pathType |
Как сравнивается | /foo совпадает с |
|---|---|---|
Exact |
путь целиком, с учётом регистра | только /foo (не /foo/) |
Prefix |
по элементам, разделённым / |
/foo, /foo/, /foo/bar, но не /foobar |
ImplementationSpecific |
как решит контроллер | зависит от контроллера |
Prefix по спецификации сравнивает не строки, а элементы пути: /aaa/bb не совпадает с /aaa/bbb. Если подходят несколько путей, побеждает самый длинный, а при равной длине — Exact. ImplementationSpecific лучше не использовать: переносимость манифеста между контроллерами пропадает, а поведение приходится искать в документации конкретного контроллера. Но и Prefix контроллеры понимают не всегда по спецификации: Traefik по умолчанию сравнивает его как строку, и /health у него совпадает с /healthz. Ниже мы включим строгое сравнение и проверим разницу на стенде.
backend — Service и его порт, числом (number) или именем (name). Ingress ссылается только на сервисы в своём неймспейсе. defaultBackend на уровне spec — куда отправлять запросы, не подошедшие ни под одно правило; без него контроллер ответит своим 404.
tls — список имён и Secret типа kubernetes.io/tls с сертификатом и ключом для них. Secret обязан лежать в том же неймспейсе, что и Ingress. Это значит, что каждая команда хранит у себя копию ключа — в том числе ключа wildcard-сертификата, если он общий на все сайты. Запомним это: Gateway API решает вопрос иначе. И ещё: раздел tls не запрещает обычный HTTP. Что делать с запросом на порт 80 для такого сайта, решает контроллер; Traefik без дополнительных аннотаций отдаёт его по HTTP так же, как по HTTPS.
IngressClass связывает Ingress с конкретным контроллером. В кластере может работать несколько контроллеров, и каждый должен понимать, какие Ingress его:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: traefik
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: traefik.io/ingress-controller
spec.controller — строка, которую контроллер ищет у себя: Traefik обслуживает классы с traefik.io/ingress-controller. Ingress выбирает класс полем ingressClassName. Аннотация is-default-class делает класс классом по умолчанию: admission-плагин API-сервера DefaultIngressClass (admission-плагины проверяют и дополняют объекты до записи в etcd) проставит его в новые Ingress без ingressClassName. Старая аннотация kubernetes.io/ingress.class на самом Ingress устарела, и в новых манифестах её не пишут.
Всё, что выходит за пределы host + path + TLS — перенаправления, переписывание путей, таймауты, заголовки, авторизация, — в Ingress не предусмотрено. Каждый контроллер добавлял это своими аннотациями: у ingress-nginx это nginx.ingress.kubernetes.io/..., у Traefik — traefik.ingress.kubernetes.io/.... Аннотации не проверяются схемой API, не переносятся между контроллерами и копятся годами. API Ingress заморожен: он остаётся GA, удалять его из Kubernetes не собираются, но развиваться он больше не будет, и документация Kubernetes рекомендует Gateway API.
Ingress-контроллер
Ingress-контроллер — это обычное приложение в кластере (Deployment или DaemonSet) с двумя ролями: он контроллер в смысле части 3 и он же прокси, через который идёт трафик.
Как контроллер он открывает к API-серверу watch на нужные ему ресурсы: IngressClass, Ingress, Service, EndpointSlice, Secret (для сертификатов) и Namespace (для правил, выбирающих неймспейсы по меткам), а если включён Gateway API — ещё GatewayClass, Gateway, HTTPRoute, GRPCRoute, ReferenceGrant и другие ресурсы этой группы. Для этого его ServiceAccount получает ClusterRole с get, list, watch на эти ресурсы и update на подресурс status у Ingress, шлюзов и маршрутов. Клиентская библиотека держит локальный кэш объектов (информер): сначала list, потом поток событий ADDED, MODIFIED, DELETED. На каждое событие контроллер пересобирает свою модель маршрутизации и применяет её.
Как прокси он слушает порты и маршрутизирует запросы по этой модели. Важная деталь: большинство контроллеров, Traefik в том числе, не ходят через ClusterIP сервиса. Service нужен им только как способ найти поды: из EndpointSlice берутся адреса готовых подов, и Traefik балансирует между ними сам. kube-proxy на этом участке не участвует, а под, переставший проходить readiness-пробу, исчезает из EndpointSlice и тут же — из маршрутов прокси.
Как применяется новая конфигурация, зависит от прокси. Контроллеры на nginx исторически генерировали nginx.conf и делали перезагрузку, что на больших кластерах означает частые перезапуски рабочих процессов. Traefik хранит конфигурацию в памяти и меняет её на лету, без перезагрузки и без разрыва соединений.
Наконец, контроллер пишет обратно в API: в status.loadBalancer.ingress Ingress — адрес, на котором он доступен (он копирует его из status своего сервиса LoadBalancer), в status Gateway и маршрутов — условия, о которых ниже. Событий (Events) по Ingress, Gateway и маршрутам Traefik не пишет: всё, что он хочет сказать, — в status и в его журнале.
Контроллеров много. ingress-nginx — контроллер на основе веб-сервера nginx, созданный в самом проекте Kubernetes как эталонная реализация Ingress. Traefik — обратный прокси на Go от компании Traefik Labs, который сам читает конфигурацию из API Kubernetes и умеет и Ingress, и Gateway API, и собственные CRD. Envoy Gateway — реализация Gateway API на прокси Envoy (высокопроизводительный прокси из CNCF — фонда Cloud Native Computing Foundation, где развиваются Kubernetes и связанные проекты; на Envoy построены и многие сервис-меши). NGINX Gateway Fabric — реализация Gateway API от F5 на nginx. Cilium — CNI-плагин на eBPF (программах, которые ядро Linux выполняет на событиях вроде прихода пакета), в который встроена своя реализация Gateway API. Выбор — дело вкуса и требований; в нашей серии Traefik.
Судьба ingress-nginx
Долгие годы самым распространённым контроллером был ingress-nginx. Его гибкость обернулась долгом: аннотации-«сниппеты», вставляющие произвольные директивы nginx, со временем стали считаться опасными, а поддерживали проект годами один-два человека в свободное время. В ноябре 2025 года SIG Network и комитет по безопасности Kubernetes объявили, что проект уходит на покой: поддержка в режиме best-effort до марта 2026 года, затем — никаких выпусков, исправлений ошибок и исправлений уязвимостей. В январе 2026 года это заявление повторили руководящий комитет и комитет по безопасности с прямым предупреждением: оставаться на ingress-nginx после этого — значит оставаться открытым для атак. Последние выпуски контроллера, 1.15.1 и 1.14.5, вышли 19 марта 2026 года, после чего репозиторий kubernetes/ingress-nginx переведён в архив, только для чтения.
Что это значит на практике:
- существующие установки продолжают работать, образы и чарты остаются доступными — ничего не сломается в один день, но и уязвимости больше никто не закроет;
- проверить, стоит ли он в кластере, можно одной командой:
kubectl get pods -A -l app.kubernetes.io/name=ingress-nginx; - готовой замены «один в один» нет: рекомендованный путь — Gateway API, для переноса манифестов в SIG Network сделан инструмент
ingress2gateway, который в версии 1.0 (март 2026) понимает больше тридцати аннотаций ingress-nginx и предупреждает о тех, что перевести нельзя; - сам API Ingress не удаляется: можно остаться на Ingress и сменить контроллер. В Traefik для этого есть даже отдельный провайдер, который читает Ingress с аннотациями ingress-nginx.
Не путать с NGINX Ingress Controller от F5 — это отдельный продукт с похожим названием, к уходу ingress-nginx отношения не имеет.
Gateway API
Gateway API — набор CRD от SIG Network, который вырос из опыта Ingress. Он не встроен в Kubernetes, а ставится отдельно, как обычные CRD, и развивается своими выпусками: версия 1.0 вышла в 2023 году, на момент написания актуальна 1.6.3. Ресурсы делятся на два канала: Standard — стабильные, с гарантией совместимости, и Experimental — то, что ещё может поменяться.
Главная идея — разделение ролей. В Ingress одна команда в одном объекте описывает и то, на каком адресе принимать трафик, и какой сертификат показывать, и куда отправлять запросы. В Gateway API это три уровня, и каждым владеет свой человек:
GatewayClass "traefik" ◄── поставщик инфраструктуры:
controllerName: какой контроллер реализует шлюзы
traefik.io/gateway-controller
▲
│ gatewayClassName
Gateway "apps" (ns traefik) ◄── администратор кластера:
listener http :8000 порты, протоколы, имена,
listener https :8443 + TLS сертификаты, кого пускать
▲
│ parentRefs
HTTPRoute "hello" (ns demo) ◄── команда приложения:
hostnames, matches, куда отправлять свои запросы
filters, backendRefs ────────────► Service hello
GatewayClass — кластерный ресурс, аналог IngressClass: имя класса и controllerName, по которому контроллер узнаёт свои классы. Через parametersRef класс может ссылаться на объект с настройками, специфичными для реализации.
Gateway — точка входа трафика: набор слушателей (listeners). У каждого слушателя — имя, протокол (HTTP, HTTPS, TLS, TCP, UDP), порт, необязательное имя хоста, настройки TLS и allowedRoutes — маршруты из каких неймспейсов и каких видов могут к нему присоединиться. Сертификат указывается здесь, у шлюза, и живёт в неймспейсе администратора; командам приложений доступ к ключу не нужен.
Маршруты описывают, куда отправлять трафик:
- HTTPRoute — маршрутизация HTTP и HTTPS: имена хостов, условия (
matches) по пути, заголовкам, параметрам запроса и методу, фильтры и бэкенды с весами. - GRPCRoute — для gRPC. gRPC — фреймворк удалённого вызова процедур от Google: вызовы идут поверх HTTP/2, сообщения кодируются в Protocol Buffers, компактном двоичном формате со схемой. GRPCRoute сопоставляет запросы по имени сервиса и метода gRPC, а не по путям URL.
- TLSRoute — маршрутизация по SNI без расшифровки: в режиме
Passthroughшлюз смотрит только на имя в ClientHello и передаёт зашифрованный поток бэкенду как есть; сертификат при этом держит само приложение. Стабильным TLSRoute стал в Gateway API 1.5. - TCPRoute и UDPRoute — проброс портов TCP и UDP без разбора протокола; в Standard вошли в версии 1.6.
Не каждая реализация умеет все виды: Traefik 3.7 обслуживает HTTPRoute, GRPCRoute и TLSRoute, TCPRoute — только с опцией experimentalChannel и CRD канала Experimental, а UDPRoute не поддерживает вовсе. Что именно принимает слушатель, он сам пишет в status — поле supportedKinds, увидим его ниже.
Присоединение двустороннее. Маршрут ссылается на Gateway через parentRefs (можно уточнить конкретный слушатель — sectionName), а слушатель через allowedRoutes решает, принимать ли его. По умолчанию слушатель принимает маршруты только из своего неймспейса; разрешить другие можно всем сразу (from: All) или по меткам неймспейса (from: Selector). Так администратор контролирует, кто может публиковать сайты на общем адресе, а команды не могут «украсть» чужой шлюз. Имена хостов пересекаются: маршрут с hello.apps.example.com присоединится к слушателю с *.apps.example.com, а маршрут с hello.other.example.com — нет.
ReferenceGrant — разрешение на ссылку через границу неймспейса. Ссылаться на объекты чужого неймспейса в Gateway API можно только тогда, когда владелец того неймспейса это явно разрешил. Например, Gateway в traefik хочет взять сертификат из Secret в неймспейсе certs:
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: allow-traefik-gateways
namespace: certs # неймспейс того, НА ЧТО ссылаются
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: traefik
to:
- group: ""
kind: Secret
name: apps-example-com-tls
Без такого объекта слушатель получит условие ResolvedRefs: False с причиной RefNotPermitted — на стенде мы это проверим. То же для HTTPRoute, который хочет отправлять трафик в Service другого неймспейса. В Gateway API 1.5 ReferenceGrant получил версию v1.
Чем Gateway API лучше Ingress:
| Ingress | Gateway API | |
|---|---|---|
| Роли | один объект на всё | класс, шлюз и маршруты у разных владельцев |
| Сертификаты | Secret в каждом неймспейсе приложения | у шлюза, в неймспейсе администратора |
| Перенаправления, заголовки, переписывание, зеркалирование, веса | аннотации конкретного контроллера | стандартные фильтры и поля |
| Условия маршрута | host и путь | путь, заголовки, параметры, метод |
| Протоколы | HTTP и HTTPS | HTTP, gRPC, TLS, TCP, UDP |
| Межнеймспейсные ссылки | запрещены | по явному ReferenceGrant |
| Обратная связь | адрес в status |
условия на шлюзе, слушателях и каждом маршруте |
| Развитие | заморожен | новые выпуски несколько раз в год |
Платой за это служит многословность: простой сайт в Gateway API — это два объекта вместо одного, а для перенаправления на HTTPS — три.
Установка Traefik
Gateway API не входит в Kubernetes, поэтому сначала ставим его CRD — официальным манифестом канала Standard. Traefik 3.7 поддерживает спецификацию 1.6.3:
$ kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/listenersets.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/tcproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/tlsroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/udproutes.gateway.networking.k8s.io serverside-applied
validatingadmissionpolicy.admissionregistration.k8s.io/safe-upgrades.gateway.networking.k8s.io serverside-applied
validatingadmissionpolicybinding.admissionregistration.k8s.io/safe-upgrades.gateway.networking.k8s.io serverside-applied
Кроме уже знакомых ресурсов, в канале Standard ещё два: BackendTLSPolicy — как шлюзу проверять сертификат бэкенда, если до пода тоже идёт TLS, и ListenerSet — слушатели, которые можно добавить к чужому Gateway отдельным объектом в своём неймспейсе. А пара safe-upgrades — правило ValidatingAdmissionPolicy (проверки на языке CEL, которые API-сервер выполняет сам, без вебхука): оно не даёт поставить CRD канала Experimental поверх Standard и откатить Gateway API на версию старше 1.5.
--server-side рекомендует сам проект: при обычном apply kubectl кладёт копию объекта в аннотацию last-applied-configuration, а у аннотаций предел 256 КиБ. CRD канала Experimental в него уже не помещаются, а самый большой CRD канала Standard, httproutes, на стенде занимает в компактном JSON около 240 КБ — почти впритык.
Сам Traefik у проекта распространяется Helm-чартом, и ставить его россыпью манифестов (Deployment, ServiceAccount, ClusterRole, CRD, Service) неудобно. Поэтому здесь одна команда Helm — менеджера пакетов для Kubernetes, где пакет (чарт) состоит из шаблонов манифестов, а параметры подставляются из файла значений (values). Как устроены чарты, релизы и откаты, подробно — в части 27. Файл traefik-values.yaml:
deployment:
replicas: 2
providers:
kubernetesIngress:
enabled: true
strictPrefixMatching: true # Prefix по элементам пути, как в спецификации
kubernetesCRD:
enabled: true # CRD Traefik: Middleware и другие
kubernetesGateway:
enabled: true # Gateway API
ingressClass:
enabled: true
isDefaultClass: true
gateway:
enabled: false # Gateway опишем сами, манифестом
service:
annotations:
metallb.io/loadBalancerIPs: 192.168.10.200
spec:
externalTrafficPolicy: Local
accessLog:
enabled: true
И установка с фиксированной версией чарта (чарт 42.1.3 ставит Traefik v3.7.16):
$ helm repo add traefik https://traefik.github.io/charts
$ helm install traefik traefik/traefik --version 42.1.3 \
--namespace traefik --create-namespace -f traefik-values.yaml
Чарт ставит из своего каталога crds/ 25 CRD — собственные ресурсы Traefik в группе traefik.io (Middleware, IngressRoute, TLSOption и другие) и ресурсы платной редакции Traefik Hub в группе hub.traefik.io; CRD Gateway API в чарт больше не входят — их мы поставили сами. Что получилось:
$ kubectl -n traefik get deploy,svc
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/traefik 2/2 2 2 45s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/traefik LoadBalancer 10.103.63.116 192.168.10.200 80:30201/TCP,443:31723/TCP 46s
$ kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
traefik (default) traefik.io/ingress-controller <none> 45s
$ kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
traefik traefik.io/gateway-controller True 44s
(default) у IngressClass — та самая аннотация is-default-class. GatewayClass чарт создаёт и при gateway.enabled: false: выключен только шлюз по умолчанию. ACCEPTED True значит, что Traefik признал класс своим.
Несколько вещей, которые чарт делает хорошо по умолчанию и которые стоит знать. Traefik слушает внутри пода непривилегированные порты 8000 (точка входа web) и 8443 (websecure), а Service сопоставляет им 80 и 443; поэтому контейнеру не нужна capability NET_BIND_SERVICE, он работает от UID 65532 с файловой системой только для чтения и без всех capabilities. На websecure TLS включён для всех маршрутов сразу (--entryPoints.websecure.http.tls=true). Панель управления Traefik (dashboard) включена, но наружу не опубликована; посмотреть её можно через kubectl port-forward, а публиковать без аутентификации не стоит.
Строгий Prefix. strictPrefixMatching в values — не украшение: без него провайдер Ingress у Traefik сравнивает pathType: Prefix посимвольно, как строку, и правило /health перехватывает /healthz и /healthy/x. С опцией сравнение идёт по элементам пути, как требует спецификация. На стенде мы проверим оба поведения.
Адрес клиента. externalTrafficPolicy: Local говорит kube-proxy не пересылать трафик с узла на узел, а отдавать его только подам на том узле, куда он пришёл. С политикой Cluster kube-proxy раздаёт соединения подам Traefik на всех узлах и при этом подменяет адрес источника на адрес узла, на который пришёл пакет, — даже когда под стоит на том же узле. На стенде с Cluster все запросы с ноутбука попадали в журналы обоих подов Traefik с адресом 192.168.10.21 — узла-лидера k8s-node-2, а с Local в журнале стоит адрес самого ноутбука. В режиме L2 у MetalLB есть следствие: адрес объявляет один узел, и объявлять его может только узел с готовым подом Traefik, так что весь трафик идёт в под на одном узле, а второй под ждёт на случай отказа. Как это устроено при анонсе адреса по BGP, когда трафик делится между всеми узлами с подами Traefik, подробно разобрано в части 7 серии про Kubernetes на BGP.
Gateway и сертификат
Сертификаты по-настоящему будет выпускать cert-manager в следующей части. Пока создадим самоподписанный wildcard-сертификат, чтобы было что показать в HTTPS:
$ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
-subj "/CN=*.apps.example.com" -addext "subjectAltName=DNS:*.apps.example.com" \
-keyout apps.key -out apps.crt
$ kubectl -n traefik create secret tls apps-example-com-tls --cert=apps.crt --key=apps.key
secret/apps-example-com-tls created
Secret лежит в неймспейсе traefik, рядом со шлюзом, и командам приложений к нему доступ не нужен. Теперь шлюз:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: apps
namespace: traefik
spec:
gatewayClassName: traefik
listeners:
- name: http
protocol: HTTP
port: 8000
hostname: "*.apps.example.com"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway.example.com/apps: "allowed"
- name: https
protocol: HTTPS
port: 8443
hostname: "*.apps.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: apps-example-com-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway.example.com/apps: "allowed"
Порты слушателей у Traefik — это порты его точек входа внутри пода, 8000 и 8443, а не 80 и 443 сервиса: если порт слушателя не совпадает ни с одной точкой входа, Traefik отмечает слушатель как непринятый с причиной PortUnavailable (на стенде увидим и это). Маршруты принимаются только из неймспейсов с меткой gateway.example.com/apps=allowed; пометим demo:
$ kubectl label namespace demo gateway.example.com/apps=allowed
namespace/demo labeled
Метки неймспейса может ставить только тот, у кого есть права на сам объект Namespace, — обычно администратор, а не команда приложения. Так публикация на общем адресе остаётся под контролем.
HTTPRoute и перенаправление на HTTPS
В неймспейсе demo работает Deployment hello и Service hello с портом 80, который ведёт на порт 8080 подов. Маршрут для HTTPS:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello
namespace: demo
spec:
parentRefs:
- name: apps
namespace: traefik
sectionName: https
hostnames:
- hello.apps.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: hello
port: 80
backendRefs можно перечислить несколько, с полем weight — так делается канареечный выпуск: 90 % трафика на hello, 10 % на hello-v2, без аннотаций и без отдельных объектов.
На слушатель http повесим второй маршрут, который ничего не проксирует, а только перенаправляет:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello-redirect
namespace: demo
spec:
parentRefs:
- name: apps
namespace: traefik
sectionName: http
hostnames:
- hello.apps.example.com
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Фильтр RequestRedirect меняет схему, и по спецификации порт в Location при этом берётся стандартный для новой схемы — 443, то есть порт 8443 внутри пода наружу не просочится; путь и параметры запроса сохраняются. Код по умолчанию — 302 (временное перенаправление); для «всегда ходи по HTTPS» правильнее постоянное, 301.
Есть и второй способ — включить перенаправление на всей точке входа web в values Traefik (ports.web.http.redirections.entryPoint с to: websecure). Он проще, но касается сразу всех сайтов и всех API, и у него есть тонкость с выпуском сертификатов по HTTP, о которой поговорим в следующей части. Маршрут с фильтром явнее и переносим между контроллерами.
Для сравнения — то же через Ingress. TLS-сертификат для Ingress должен лежать в demo, а точку входа и TLS задают аннотации Traefik — иначе, как мы видели, сайт будет открываться и по HTTP:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
namespace: demo
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- hello-ingress.apps.example.com
secretName: hello-ingress-tls
rules:
- host: hello-ingress.apps.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
Перенаправление с HTTP для такого Ingress делается вторым Ingress на точке входа web с middleware redirectScheme — что такое middleware, сразу ниже:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: demo
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-redirect
namespace: demo
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
traefik.ingress.kubernetes.io/router.middlewares: demo-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: hello-ingress.apps.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
С permanent: true Traefik выбирает код по методу запроса: на GET отвечает 301 (Moved Permanently), на все остальные методы — 308 (Permanent Redirect); без permanent — соответственно 302 и 307. Оба кода постоянные, разница в методе: 301 разрешает клиенту повторить POST уже как GET, а 308 обязывает сохранить метод. Для GET это ничего не меняет, поэтому браузер, открывающий страницу, получит привычный 301.
Middleware: заголовки и ограничение частоты
Middleware у Traefik — звено обработки между приёмом запроса и отправкой его бэкенду: может поменять запрос, ответ или отказать. Middleware описываются ресурсом Middleware из CRD Traefik (группа traefik.io, версия v1alpha1), который чарт поставил вместе с собой. Создадим два в неймспейсе demo.
Заголовки безопасности ответа:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: demo
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin
customResponseHeaders:
X-Powered-By: ""
stsSeconds добавляет заголовок Strict-Transport-Security (HSTS): браузер запомнит на год, что сайт открывается только по HTTPS, и даже не попробует HTTP. contentTypeNosniff — X-Content-Type-Options: nosniff, запрет браузеру угадывать тип содержимого. frameDeny — X-Frame-Options: DENY, запрет встраивать сайт во фрейм чужой страницы. referrerPolicy — заголовок Referrer-Policy: на чужие сайты браузер передаёт в Referer только адрес сайта, без пути. Пустое значение в customResponseHeaders удаляет заголовок: не нужно рассказывать посетителям, на чём написано приложение. HSTS включают только тогда, когда HTTPS действительно работает: ошибку в нём браузеры будут помнить весь срок.
Ограничение частоты запросов:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: demo
spec:
rateLimit:
average: 20
period: 1s
burst: 40
Это «ведро с токенами»: в среднем 20 запросов в секунду, всплеском до 40; сверх этого Traefik отвечает 429 Too Many Requests, не беспокоя приложение. Учёт ведётся по адресу клиента — и здесь пригождается externalTrafficPolicy: Local: без него все клиенты приходили бы с адреса узла и делили бы одно ведро на всех. Если перед Traefik стоит ещё один прокси, адрес нужно брать из X-Forwarded-For (sourceCriterion.ipStrategy.depth) и доверять этому заголовку только от известных адресов. И ещё: счётчики живут в памяти каждого пода Traefik отдельно; общий лимит на несколько реплик требует Redis — хранилища «ключ — значение» в памяти, где реплики держат общие счётчики (поле redis у того же middleware).
Подключение к Ingress — аннотацией со ссылкой вида <неймспейс>-<имя>@kubernetescrd (суффикс — имя провайдера, из которого взят middleware):
metadata:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: demo-secure-headers@kubernetescrd,demo-rate-limit@kubernetescrd
Подключение к HTTPRoute — двумя путями. Для заголовков в Gateway API есть стандартный фильтр ResponseHeaderModifier, и он переносим на любой контроллер. Ограничения частоты в стандарте нет, и для него служит фильтр ExtensionRef — ссылка на объект реализации, у Traefik это Middleware из того же неймспейса:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello
namespace: demo
spec:
parentRefs:
- name: apps
namespace: traefik
sectionName: https
hostnames:
- hello.apps.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: Strict-Transport-Security
value: max-age=31536000; includeSubDomains
- name: X-Content-Type-Options
value: nosniff
- name: X-Frame-Options
value: DENY
remove:
- X-Powered-By
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: rate-limit
backendRefs:
- name: hello
port: 80
Стандартные фильтры HTTPRoute — RequestHeaderModifier и ResponseHeaderModifier (заголовки), RequestRedirect, URLRewrite (переписать имя хоста или путь), RequestMirror (копия запроса на другой бэкенд, ответ которого отбрасывается), CORS (заголовки разрешений для запросов с чужих сайтов, стабилен с 1.5) и ExtensionRef. Правило простое: что есть в стандарте — через стандарт, остальное — через ExtensionRef, понимая, что эта часть манифеста привязана к Traefik.
Смотрим руками
Статус шлюза и маршрутов
Gateway API сообщает о каждом шаге условиями (conditions), и это первое место, куда смотреть, когда что-то не открывается:
$ kubectl -n traefik get gateway apps
NAME CLASS ADDRESS PROGRAMMED AGE
apps traefik 192.168.10.200 True 6m59s
$ kubectl -n traefik get gateway apps -o yaml
…
status:
addresses:
- type: IPAddress
value: 192.168.10.200
conditions:
- lastTransitionTime: "2026-10-29T12:47:09Z"
message: Gateway successfully scheduled
observedGeneration: 1
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: "2026-10-29T12:47:09Z"
message: Gateway successfully programmed
observedGeneration: 1
reason: Programmed
status: "True"
type: Programmed
listeners:
- attachedRoutes: 1
conditions:
- lastTransitionTime: "2026-10-29T12:47:09Z"
message: No error found
observedGeneration: 1
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: "2026-10-29T12:47:09Z"
message: No error found
observedGeneration: 1
reason: ResolvedRefs
status: "True"
type: ResolvedRefs
- lastTransitionTime: "2026-10-29T12:47:09Z"
message: No error found
observedGeneration: 1
reason: Programmed
status: "True"
type: Programmed
name: http
supportedKinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
- group: gateway.networking.k8s.io
kind: GRPCRoute
- attachedRoutes: 1
…
name: https
…
Accepted — контроллер признал шлюз своим и конфигурация допустима; Programmed — она применена к прокси; ResolvedRefs у слушателя — сертификат найден и прочитан; attachedRoutes — сколько маршрутов присоединилось (на http — hello-redirect, на https — hello); supportedKinds — какие виды маршрутов слушатель принимает. Адрес Traefik скопировал из status своего сервиса.
Маршрут:
$ kubectl -n demo get httproute
NAME HOSTNAMES AGE
hello ["hello.apps.example.com"] 6m46s
hello-redirect ["hello.apps.example.com"] 6m46s
$ kubectl -n demo get httproute hello -o jsonpath='{.status.parents[0]}' | jq
{
"conditions": [
{
"lastTransitionTime": "2026-10-29T12:41:05Z",
"message": "",
"observedGeneration": 2,
"reason": "Accepted",
"status": "True",
"type": "Accepted"
},
{
"lastTransitionTime": "2026-10-29T12:41:05Z",
"message": "",
"observedGeneration": 2,
"reason": "ResolvedRefs",
"status": "True",
"type": "ResolvedRefs"
}
],
"controllerName": "traefik.io/gateway-controller",
"parentRef": {
"group": "gateway.networking.k8s.io",
"kind": "Gateway",
"name": "apps",
"namespace": "traefik",
"sectionName": "https"
}
}
(jq — утилита для разбора и форматирования JSON.) Статус у маршрута — по каждому родителю отдельно: один маршрут может висеть на нескольких шлюзах, и на одном он принят, а на другом нет. observedGeneration: 2 — статус относится ко второй версии маршрута, той, где мы добавили фильтры.
Запросы
DNS-записи для hello.apps.example.com может ещё не быть, и это не мешает: --resolve говорит curl, на какой адрес идти, при этом имя попадает и в SNI, и в Host, как если бы его нашёл DNS. --cacert — наш самоподписанный сертификат как доверенный корень. Запрос с ноутбука, строки индикатора загрузки опущены:
$ curl -v --resolve hello.apps.example.com:443:192.168.10.200 \
--cacert apps.crt https://hello.apps.example.com/
* Added hello.apps.example.com:443:192.168.10.200 to DNS cache
* Hostname hello.apps.example.com was found in DNS cache
* Trying 192.168.10.200:443...
* Connected to hello.apps.example.com (192.168.10.200) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: apps.crt
* CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
* subject: CN=*.apps.example.com
* start date: Oct 29 12:35:46 2026 GMT
* expire date: Nov 28 12:35:46 2026 GMT
* subjectAltName: host "hello.apps.example.com" matched cert's "*.apps.example.com"
* issuer: CN=*.apps.example.com
* SSL certificate verify ok.
* Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hello.apps.example.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hello.apps.example.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: hello.apps.example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< date: Thu, 29 Oct 2026 12:47:29 GMT
< strict-transport-security: max-age=31536000; includeSubDomains
< x-app-version: 1.0
< x-content-type-options: nosniff
< x-frame-options: DENY
< content-length: 34
<
* Connection #0 to host hello.apps.example.com left intact
hello from hello-6d5c6db6b4-nz5pj
Видно всё по порядку: TLS-рукопожатие с сертификатом, выбранным по SNI, согласование HTTP/2 через ALPN (расширение TLS, в котором клиент и сервер договариваются о протоколе приложения), заголовки, добавленные фильтром (x-app-version ставит само приложение), и ответ пода. До бэкенда при этом шёл обычный HTTP/1.1: протокол между клиентом и прокси и между прокси и подом выбирается независимо.
Перенаправление — путь и параметры сохраняются, порт 8443 в Location не попадает:
$ curl -sI --resolve hello.apps.example.com:80:192.168.10.200 http://hello.apps.example.com/
HTTP/1.1 301 Moved Permanently
Location: https://hello.apps.example.com/
Date: Thu, 29 Oct 2026 12:47:29 GMT
Content-Length: 17
$ curl -sI --resolve hello.apps.example.com:80:192.168.10.200 'http://hello.apps.example.com/a/b?x=1' | grep -i location
Location: https://hello.apps.example.com/a/b?x=1
Запрос на имя, которого нет ни в одном маршруте, Traefik обслуживает сам и отвечает 404 — а по TLS ещё и своим самоподписанным сертификатом TRAEFIK DEFAULT CERT, потому что для чужого SNI подходящего сертификата у него нет:
$ curl -sk -o /dev/null -w '%{http_code}\n' --resolve nope.example.com:443:192.168.10.200 https://nope.example.com/
404
$ curl -skv --resolve nope.example.com:443:192.168.10.200 https://nope.example.com/ 2>&1 | grep -E 'subject:|issuer:'
* subject: CN=TRAEFIK DEFAULT CERT
* issuer: CN=TRAEFIK DEFAULT CERT
Ограничение частоты — сто запросов в двадцать параллельных потоков:
$ seq 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' \
--resolve hello.apps.example.com:443:192.168.10.200 --cacert apps.crt \
https://hello.apps.example.com/ | sort | uniq -c
43 200
57 429
Прошли 43 запроса — сорок за счёт burst и ещё несколько за счёт пополнения ведра, пока шёл тест; остальные получили 429. Последовательный цикл из curl до лимита, скорее всего, не дотянется: каждый запрос с новым TLS-рукопожатием занимает время, и ведро успевает пополняться.
Журнал доступа
Traefik пишет его в stdout пода, по умолчанию в своём расширенном формате CLF (Common Log Format — классический однострочный формат журналов веб-серверов). Трафик с Local в режиме L2 весь идёт в под Traefik на узле-лидере k8s-node-2, поэтому смотрим его журнал — вот две строки после запроса по HTTPS и перенаправления:
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s --tail=2
192.168.10.50 - - [29/Oct/2026:12:47:29 +0000] "GET / HTTP/2.0" 200 34 "-" "-" 20 "httproute-demo-hello-gw-traefik-apps-ep-websecure-0-e329ef369d5777264fcf@kubernetesgateway" "http://172.20.211.14:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:47:29 +0000] "HEAD / HTTP/1.1" 301 17 "-" "-" 21 "httproute-demo-hello-redirect-gw-traefik-apps-ep-web-0-997f60aee86fcd0c0434@kubernetesgateway" "-" 0ms
Поля: адрес клиента — настоящий адрес ноутбука 192.168.10.50, а не адрес узла, благодаря Local; запрос и код ответа; размер; Referer и User-Agent — прочерки, потому что чарт по умолчанию не пишет в журнал заголовки запроса (accesslog.fields.headers.defaultmode=drop); номер запроса с запуска пода; имя маршрутизатора Traefik — по нему видно, из какого объекта взят маршрут (@kubernetesgateway — HTTPRoute, @kubernetes — Ingress, @kubernetescrd — CRD Traefik); адрес бэкенда — прямо адрес пода из EndpointSlice, не ClusterIP, причём под 172.20.211.14 стоит на k8s-node-1, а Traefik — на k8s-node-2; время обработки. У перенаправления бэкенда нет — ответил сам Traefik. Отказ по частоте выглядит так же, без бэкенда:
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s | grep ' 429 ' | tail -1
192.168.10.50 - - [29/Oct/2026:12:47:46 +0000] "GET / HTTP/2.0" 429 17 "-" "-" 122 "httproute-demo-hello-gw-traefik-apps-ep-websecure-0-e329ef369d5777264fcf@kubernetesgateway" "-" 0ms
Для сбора в систему журналов удобнее accessLog.format: json, об этом — в части 33.
Когда что-то не так
Маршрут в неймспейсе sandbox, у которого нет метки gateway.example.com/apps=allowed:
$ kubectl -n sandbox get httproute test -o jsonpath='{.status.parents[0].conditions}' | jq
[
{
"lastTransitionTime": "2026-10-29T12:45:06Z",
"message": "",
"observedGeneration": 1,
"reason": "NotAllowedByListeners",
"status": "False",
"type": "Accepted"
},
{
"lastTransitionTime": "2026-10-29T12:45:06Z",
"message": "Cannot load HTTPBackendRef sandbox/hello: getting service: service \"hello\" not found",
"observedGeneration": 1,
"reason": "BackendNotFound",
"status": "False",
"type": "ResolvedRefs"
}
]
Слушатель маршрут не пустил (NotAllowedByListeners), и заодно Traefik сообщил, что сервиса hello в sandbox нет: backendRefs без namespace ищет сервис в неймспейсе маршрута. Запрос на test.apps.example.com получает 404 от самого Traefik.
Ошибка в имени сервиса в разрешённом неймспейсе:
$ kubectl -n demo get httproute typo -o jsonpath='{.status.parents[0].conditions}' | jq
[
{
"lastTransitionTime": "2026-10-29T12:45:06Z",
"message": "",
"observedGeneration": 1,
"reason": "Accepted",
"status": "True",
"type": "Accepted"
},
{
"lastTransitionTime": "2026-10-29T12:45:06Z",
"message": "Cannot load HTTPBackendRef demo/helo: getting service: service \"helo\" not found",
"observedGeneration": 1,
"reason": "BackendNotFound",
"status": "False",
"type": "ResolvedRefs"
}
]
$ curl -s -w ' %{http_code}\n' --resolve typo.apps.example.com:443:192.168.10.200 --cacert apps.crt https://typo.apps.example.com/
500
Маршрут принят, но ссылку не разрешил, и запросы по нему получают 500 с пустым телом: так требует спецификация, чтобы ошибка была заметна, а не превращалась в чужую страницу 404.
Сертификат из чужого неймспейса без ReferenceGrant — шлюз certs-test с certificateRefs на Secret в certs:
$ kubectl -n traefik get gateway
NAME CLASS ADDRESS PROGRAMMED AGE
apps traefik 192.168.10.200 True 5m13s
certs-test traefik 192.168.10.200 False 4s
$ kubectl -n traefik get gateway certs-test -o jsonpath='{.status.listeners[0].conditions}' | jq -c '.[] | {type,status,reason,message}'
{"type":"ResolvedRefs","status":"False","reason":"RefNotPermitted","message":"Cannot reference CertificateRef certs/apps-example-com-tls: missing ReferenceGrant"}
{"type":"Programmed","status":"False","reason":"Invalid","message":"Invalid CertificateRefs"}
Сам шлюз при этом Accepted: False с причиной ListenersNotValid — у него нет ни одного годного слушателя. После kubectl apply ReferenceGrant из раздела про Gateway API те же условия меняются через несколько секунд:
$ kubectl -n traefik get gateway certs-test -o jsonpath='{.status.listeners[0].conditions}' | jq -c '.[] | {type,status,reason,message}'
{"type":"Accepted","status":"True","reason":"Accepted","message":"No error found"}
{"type":"ResolvedRefs","status":"True","reason":"ResolvedRefs","message":"No error found"}
{"type":"Programmed","status":"True","reason":"Programmed","message":"No error found"}
И слушатель на порту, которого нет среди точек входа Traefik, — шлюз port-test с port: 80:
$ kubectl -n traefik get gateway port-test -o jsonpath='{.status.listeners[0].conditions}' | jq -c '.[] | {type,status,reason,message}'
{"type":"Accepted","status":"False","reason":"PortUnavailable","message":"Cannot find entryPoint for Gateway: no matching entryPoint for port 80 and protocol \"HTTP\""}
Ingress на стенде
Для Ingress статус беднее — один адрес:
$ kubectl -n demo get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
hello traefik hello-ingress.apps.example.com 192.168.10.200 80, 443 5m5s
hello-redirect traefik hello-ingress.apps.example.com 192.168.10.200 80 60s
Первый вариант Ingress hello из раздела про объект — с tls, но без аннотаций Traefik — отвечал на обоих портах: по HTTPS со своим сертификатом, а по HTTP обычным ответом пода:
$ curl -s -o /dev/null -w '%{http_code}\n' --resolve hello-ingress.apps.example.com:80:192.168.10.200 http://hello-ingress.apps.example.com/
200
С аннотациями router.entrypoints: websecure, router.tls: "true" и middleware secure-headers и rate-limit порт 80 для этого имени отвечает 404, а после второго Ingress hello-redirect — перенаправлением. Код в выводе 308, а не 301, потому что curl -I шлёт запрос HEAD, а redirectScheme отвечает 301 только на GET; браузер, открывающий страницу методом GET, получит 301. У фильтра RequestRedirect выше код задан явно и от метода не зависит, поэтому там и на HEAD пришёл 301:
$ curl -sI --resolve hello-ingress.apps.example.com:80:192.168.10.200 http://hello-ingress.apps.example.com/
HTTP/1.1 308 Permanent Redirect
Location: https://hello-ingress.apps.example.com/
Date: Thu, 29 Oct 2026 12:46:22 GMT
Content-Length: 18
$ curl -sI --resolve hello-ingress.apps.example.com:443:192.168.10.200 --cacert hello-ingress.crt https://hello-ingress.apps.example.com/
HTTP/2 200
content-type: text/plain; charset=utf-8
date: Thu, 29 Oct 2026 12:46:22 GMT
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000; includeSubDomains
x-app-version: 1.0
x-content-type-options: nosniff
x-frame-options: DENY
content-length: 34
Prefix по-строчному и по-настоящему. Ingress prefix-test без ingressClassName с одним правилом: host: prefix.apps.example.com, path: /health, pathType: Prefix. Класс ему проставил admission-плагин:
$ kubectl -n demo get ingress prefix-test -o jsonpath='{.spec.ingressClassName}{"\n"}'
traefik
Запросы на /health, /health/, /healthz, /healthy/x и /. Приложение hello само отвечает 404 на /health, поэтому смотрим, кто ответил, по журналу: есть имя маршрутизатора и адрес пода — запрос дошёл до приложения, прочерки — ответил сам Traefik. Сначала Traefik без strictPrefixMatching:
$ kubectl -n traefik logs traefik-6bdf66f55f-cgqpw --tail=5
192.168.10.50 - - [29/Oct/2026:12:43:00 +0000] "GET /health HTTP/1.1" 404 19 "-" "-" 220 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.172.37:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:43:00 +0000] "GET /health/ HTTP/1.1" 404 19 "-" "-" 221 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.172.42:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:43:00 +0000] "GET /healthz HTTP/1.1" 200 3 "-" "-" 222 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.211.14:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:43:00 +0000] "GET /healthy/x HTTP/1.1" 404 19 "-" "-" 223 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.172.42:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:43:00 +0000] "GET / HTTP/1.1" 404 19 "-" "-" 224 "-" "-" 0ms
Правило /health забрало и /healthz (приложение ответило на него ok), и /healthy/x — сравнение посимвольное. Те же запросы после включения strictPrefixMatching: true:
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s | grep 'GET /'
192.168.10.50 - - [29/Oct/2026:12:44:11 +0000] "GET /health HTTP/1.1" 404 19 "-" "-" 1 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.211.14:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:11 +0000] "GET /health/ HTTP/1.1" 404 19 "-" "-" 2 "demo-prefix-test-prefix-apps-example-com-health@kubernetes" "http://172.20.172.42:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:11 +0000] "GET /healthz HTTP/1.1" 404 19 "-" "-" 3 "-" "-" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:11 +0000] "GET /healthy/x HTTP/1.1" 404 19 "-" "-" 4 "-" "-" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:11 +0000] "GET / HTTP/1.1" 404 19 "-" "-" 5 "-" "-" 0ms
Теперь /healthz и /healthy/x правилу не принадлежат, как в таблице из спецификации.
defaultBackend. Ingress fallback без правил, только с spec.defaultBackend на сервис hello (порт http):
$ kubectl -n demo get ingress fallback
NAME CLASS HOSTS ADDRESS PORTS AGE
fallback traefik * 192.168.10.200 80 3s
$ curl -s -w ' %{http_code}\n' --resolve nope.example.com:80:192.168.10.200 http://nope.example.com/
hello from hello-6d5c6db6b4-zwzsp
200
$ curl -s -w ' %{http_code}\n' --resolve prefix.apps.example.com:80:192.168.10.200 http://prefix.apps.example.com/
hello from hello-6d5c6db6b4-7tjjb
200
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s --tail=4
192.168.10.50 - - [29/Oct/2026:12:44:32 +0000] "GET / HTTP/1.1" 200 34 "-" "-" 6 "default-router@kubernetes" "http://172.20.172.42:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:32 +0000] "GET / HTTP/2.0" 200 34 "-" "-" 7 "websecure-default-router@kubernetes" "http://172.20.211.14:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:32 +0000] "GET / HTTP/1.1" 200 34 "-" "-" 8 "default-router@kubernetes" "http://172.20.172.37:8080" 0ms
192.168.10.50 - - [29/Oct/2026:12:44:32 +0000] "GET / HTTP/1.1" 301 17 "-" "-" 9 "httproute-demo-hello-redirect-gw-traefik-apps-ep-web-0-997f60aee86fcd0c0434@kubernetesgateway" "-" 0ms
Запасной маршрутизатор Traefik (default-router на web и websecure-default-router на websecure) получает всё, что не подошло ни под одно правило: чужое имя и по HTTP, и по HTTPS (вторая строка — тот же nope.example.com через curl -k), и путь /, которого нет в правилах prefix-test. Маршруты с точным совпадением имени, как hello-redirect, он не трогает. В общем кластере такой Ingress забирает весь «ничей» трафик, поэтому после опыта мы его удалили.
Ingress на несуществующий сервис. Ingress broken с правилом на сервис nope:
$ kubectl -n demo describe ingress broken | sed -n '/Rules/,$p'
Rules:
Host Path Backends
---- ---- --------
broken.apps.example.com
/ nope:80 (<error: services "nope" not found>)
Annotations: <none>
Events: <none>
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s | grep 'Cannot create service'
2026-10-29T12:44:43Z ERR Cannot create service error="service not found" ingress=broken namespace=demo providerName=kubernetes serviceName=nope servicePort=&ServiceBackendPort{Name:,Number:80,}
2026-10-29T12:44:43Z ERR Cannot create service error="service not found" ingress=broken namespace=demo providerName=kubernetes serviceName=nope servicePort=&ServiceBackendPort{Name:,Number:80,}
Ошибку в describe вычислил сам kubectl, найдя сервис, — от контроллера в Ingress ничего нет, событий тоже. Адрес в status Traefik при этом всё равно проставил, а запрос на broken.apps.example.com получает 404: маршрут Traefik просто не создал. Узнать причину можно только из его журнала. Там же попадаются строки Error while updating ingress status … the object has been modified — обе реплики Traefik пишут status одного и того же Ingress, и одна из них проигрывает гонку; это безвредно.
И напоследок — какие источники конфигурации Traefik читает из API. Каждый из них — отдельный провайдер со своим watch, и в журнале при старте видно, какие запущены:
$ kubectl -n traefik logs traefik-6b7fb4b765-w9l4s | grep 'Starting provider'
2026-10-29T12:43:32Z INF Starting provider aggregator *aggregator.ProviderAggregator
2026-10-29T12:43:32Z INF Starting provider *traefik.Provider
2026-10-29T12:43:32Z INF Starting provider *crd.Provider
2026-10-29T12:43:32Z INF Starting provider *ingress.Provider
2026-10-29T12:43:32Z INF Starting provider *gateway.Provider
2026-10-29T12:43:32Z INF Starting provider *acme.ChallengeTLSALPN
*ingress.Provider читает Ingress, *crd.Provider — CRD Traefik, *gateway.Provider — Gateway API; *traefik.Provider — внутренние маршруты самого Traefik (панель, проверка здоровья), *acme.ChallengeTLSALPN — обработчик проверок ACME, если сертификаты выпускает сам Traefik.
Источники
- Kubernetes: Ingress — правила,
pathTypeи таблица совпадений, маски в именах,defaultBackend, TLS, IngressClass и класс по умолчанию, заморозка API. - Kubernetes: Ingress Controllers — список контроллеров и несколько контроллеров в одном кластере.
- Kubernetes: Gateway API — модель ресурсов и роли.
- Kubernetes: Admission Control — плагин
DefaultIngressClass. - Ingress NGINX Retirement: What You Need to Know, Ingress NGINX: Statement from the Kubernetes Steering and Security Response Committees и Announcing Ingress2Gateway 1.0 — уход ingress-nginx и инструмент переноса; kubernetes/ingress-nginx — репозиторий в архиве; ingress2gateway.
- Gateway API — проект; API Overview и Roles and Personas — ресурсы и роли; Versioning — каналы Standard и Experimental; Security Model —
allowedRoutesи выбор неймспейсов по меткам. - Gateway API: Gateway, HTTPRoute, TLSRoute, ReferenceGrant и BackendTLSPolicy — справочник ресурсов, условия статуса, веса бэкендов.
- Gateway API: HTTP redirects and rewrites, TLS и HTTP CORS — фильтр
RequestRedirectи коды, режимы TLS, фильтр CORS. - Gateway API: Migrating from Ingress и Migrating from Ingress-NGINX — перенос манифестов.
- Gateway API: Implementations и поддержка версии 1.6 — реализации и их соответствие спецификации; Gateway API v1.6.3 — выпуск и манифест
standard-install.yaml. - Traefik: Kubernetes Ingress provider —
strictPrefixMatching,allowEmptyServices, публикация адреса вstatus; Ingress routing configuration — аннотацииtraefik.ingress.kubernetes.io/router.*. - Traefik: Kubernetes Gateway API provider — поддерживаемая версия 1.6.3 и виды маршрутов,
experimentalChannel; Gateway API routing configuration —ExtensionRefна Middleware. - Traefik: Headers, RateLimit и RedirectScheme — middleware из статьи; pkg/middlewares/redirect/redirect.go — выбор кода перенаправления в
redirectSchemeиredirectRegex: сpermanent: true301 для GET и 308 для остальных методов, без него 302 и 307 (документация этого не расписывает); Logs and Access Logs — формат журнала доступа и фильтры полей. - Traefik Helm Chart — чарт, его values и CRD.
- RFC 6066 — SNI; RFC 7301 — ALPN; RFC 6797 — HSTS; RFC 9110 — семантика HTTP, коды 301, 302, 307 и 308; RFC 9113 — HTTP/2.
Один адрес, один прокси, маршруты по имени и пути, перенаправление на HTTPS и общие политики — но сертификат пока самоподписанный и выпущен руками на месяц.
Дальше — cert-manager: как кластер сам получает сертификаты у Let's Encrypt по протоколу ACME и у собственного внутреннего центра сертификации, какие объекты при этом появляются в API и как они продлеваются без участия человека.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.