Когда проект Kubernetes только начинал свой путь, вопрос как пустить трафик в кластер решался просто: как-нибудь. Сервисы торчали наружу через NodePort, потом появился LoadBalancer, а чуть позже — объект Ingress, который на долгие годы стал стандартной точкой входа в HTTP/S-мир Kubernetes.

Ingress был своевременным решением. Он дал декларативный способ описывать маршрутизацию, TLS и виртуальные хосты, не заставляя инженеров напрямую настраивать nginx-конфиги или HAProxy руками. Для своего времени — шаг вперёд, и весьма заметный. Проблема в том, что Kubernetes рос быстрее, чем сам Ingress.

Со временем выяснилось, что спецификация Ingress намеренно минималистична. В ней нет ни чёткого разделения ответственности, ни расширяемой модели, ни нормального способа описывать сложные сценарии маршрутизации. Всё, что выходило за рамки базового use case, уезжало в аннотации ingress-контроллеров. В результате у нас появился единый стандарт, который на практике вёл себя по-разному в зависимости от того, какой контроллер стоял в кластере. Формально — Ingress, фактически — vendor-specific конфигурация с YAML-обвязкой.

На этом этапе Kubernetes-сообщество сделало важный вывод: Ingress нельзя бесконечно чинить, не ломая обратную совместимость. Поэтому он был переведён в режим feature freeze — объект остаётся, работает, но новых возможностей в него больше не добавляют. И это был не конец истории, а её поворотный момент. Ниже уведомление и сам commit с замораживаем функциональности Ingress API.

The Kubernetes project recommends using Gateway instead of Ingress. The Ingress API has been frozen.

This means that:

  • The Ingress API is generally available, and is subject to the stability guarantees for generally available APIs. The Kubernetes project has no plans to remove Ingress from Kubernetes.

  • The Ingress API is no longer being developed, and will have no further changes or updates made to it.

Так появился Gateway API — не как Ingress 2.0, а как попытка переосмыслить сам подход к управлению сетевым трафиком в Kubernetes. Проект вырос внутри SIG Network и опирается на реальные боли продакшена: multi-tenant кластеры, разделение ролей между платформенной командой и командами приложений, прозрачная работа с TLS и расширяемость без аннотационного зоопарка.

Gateway API предлагает более строгую и выразительную модель: отдельные объекты для инфраструктуры, точки входа и маршрутов, явные границы ответственности и поддержку не только HTTP, но и L4-сценариев. Это уже не просто "как прокинуть трафик внутрь", а полноценный API для управления входящими и межсервисными потоками.

В этой статье разберёмся, почему Ingress логично дошёл до своего предела, что именно предлагает Gateway API взамен и как на него переезжать без героических ночных релизов и внезапных "502 Bad Gateway".

Архитектурная модель Gateway API

Ключевое отличие Gateway API от Ingress — это не новые поля в YAML и не расширенный синтаксис, а другая архитектурная модель. Вместо одного объекта на всё Gateway API вводит несколько чётко разделённых сущностей, каждая из которых отвечает за свою часть сетевого контура. Это выглядит более многословно на бумаге, но на практике резко снижает количество неявных договорённостей между командами.

В основе модели лежит разделение на три уровня. GatewayClass описывает какой именно контроллер обслуживает трафик: NGINX, Envoy, HAProxy, Cilium — неважно. Это зона ответственности платформенной команды. Gateway — конкретная точка входа: listener’ы, порты, TLS, политика доступа. А Route-объекты (HTTPRoute, TCPRoute, TLSRoute, GRPCRoute) описывают, куда и как направлять трафик. Их уже могут создавать команды приложений, не имея доступа к инфраструктурным настройкам.

За счёт такого разделения Gateway API не навязывает единственный способ организации входного трафика. Gateway может быть как централизованным — один на весь кластер, так и распределённым — отдельный Gateway на namespace или группу сервисов. В первом случае платформа получает единую точку контроля и стандартные поли��ики, во втором — более жёсткую изоляцию и снижение blast radius. Оба варианта укладываются в спецификацию и не требуют костылей или договорённостей на словах.

Таким образом решается одна из главных проблем Ingress — отсутствие разделения ответственности. Раньше любой Ingress фактически лез в конфигурацию общего ingress-контроллера: менял TLS, хосты, аннотации, иногда — поведение балансировщика. В Gateway API платформа контролирует входную точку, а приложения — только маршрутизацию в пределах разрешённых правил. RBAC здесь наконец начинает работать так, как от него ожидали.

Вторая важная вещь — явная модель привязки. Route не подхватывается контроллером автоматически, а явно привязывается к Gateway через parentRefs. Это убирает гадание в стиле: почему этот Ingress вообще обслуживается и позволяет точно управлять тем, какие маршруты могут использовать конкретную точку входа, в том числе из других namespace.

Третье — расширяемость без аннотаций. Gateway API изначально проектировался так, чтобы контроллеры могли добавлять свои возможности через policy-объекты и extension-механизмы, не ломая базовую спецификацию. В результате сложные сценарии — traffic splitting, header-based routing, canary, rate limit — описываются структурированно, а не через строку с запятой и ссылкой на документацию конкретного вендора.

И, наконец, архитектура Gateway API решает проблему масштаба. Она одинаково хорошо работает как в маленьком кластере, так и в multi-tenant окружении с десятками команд и сотнями сервисов. Ingress в таких условиях быстро превращается в общий конфигурационный файл, к которому все боятся прикасаться. Gateway API, напротив, поощряет декомпозицию и делает сетевой слой Kubernetes управляемым, а не просто работающим, пока никто не трогает.

Envoy Gateway и базовая маршрутизация трафика

Чтобы не оставаться в теории, посмотрим на один из самых показательных вариантов — Envoy Gateway. Это референсная реализация Gateway API от сообщества Envoy, без лишних надстроек и с максимально прозрачным соответствием спецификации. Хороший вариант, чтобы понять саму модель, а не особенности конкретного ingress-контроллера.

Установка начинается с самого простого шага — деплой контроллера в кластер через helm или quickstart. Envoy Gateway ставится как обычный набор манифестов. На этом этапе ничего торчащего наружу ещё нет: мы лишь говорим кластеру, что теперь у него есть контроллер, способный обслуживать Gateway API.

Сначала и самое базовое - установка CRD без них Gateway API не заработает вообще никак:

kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml

Также можно установить экспериментальную версию CRD:

kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/experimental-install.yaml

Экспериментальная версия поддерживает ресурсы типа: TCPRoute, TLSRoute и UDPRoute

Установка Envoy Gateway:

helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.6.3 -n envoy-gateway-system --create-namespace

Далее необходимо создать GatewayClass:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller

После этого можно описать точку входа — объект Gateway. Здесь задаются listener’ы, порты и базовые параметры TLS. Обычно этот объект принадлежит платформенной команде и меняется редко.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: gateway-system
spec:
  gatewayClassName: envoy
  listeners:
  - name: default
    hostname: "*.example.com"
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: All

С этого момента у нас появляется управляемая точка входа, но трафик всё ещё никуда не ходит. Маршрутизация начинается с HTTPRoute.

Простая маршрутизация по заголовку Host

Классический сценарий — направить трафик с определённого хоста в конкретный сервис. В Gateway API это выглядит максимально прямолинейно: маршрут явно ссылается на Gateway и описывает правила.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: app
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
  hostnames:
    - app.example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: httpbin
      port: 8000

Здесь хорошо видно ключевое отличие от Ingress: маршрут живёт в namespace приложения, но использует Gateway из другого namespace — и это официально поддерживаемый сценарий, а не трюк с аннотациями.

Маршрутизация по пути

Более прикладной вариант — разрулить несколько сервисов за одним доменом. Например, /api и /frontend

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /api
    backendRefs:
      - name: api-service
        port: 80
  - matches:
      - path:
          type: PathPrefix
          value: /
    backendRefs:
      - name: frontend-service
        port: 80

Без сюрпризов, без неявных приоритетов и без угадывания, какой контроллер как именно интерпретирует правила. Всё описано декларативно и читается без документации на второй монитор.

TLS passthrough с TLSRoute

В Ingress работа с TLS часто превращалась в смесь secret’ов, аннотаций и догадок, где именно происходит терминация. Gateway API разделяет эти сценарии явно.

Если нам нужен TLS passthrough (например, когда приложение само управляет сертификатами), используется TLSRoute. В этом режиме Gateway принимает TLS-соединение, но не расшифровывает его, а выполняет маршрутизацию на основе SNI.

Важно учитывать, что TLSRoute на текущий момент всё ещё находится в экспериментальной стадии. Спецификация продолжает дорабатываться, а поддержка со стороны gateway-реализаций может отличаться — где-то функциональность уже есть, а где-то она работает с оговорками или вовсе отсутствует.

Ключевой момент: Gateway тоже должен быть объявлен как TLS-шлюз без терминации. Иначе passthrough просто не заработает — и это будет тот самый случай, когда вроде всё настроено правильно, но трафик упорно не ходит.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: gateway-system
spec:
  gatewayClassName: envoy
  listeners:
    - name: tls
      protocol: TLS
      port: 443
      tls:
        mode: Passthrough
        certificateRefs:
        - kind: Secret
          group: ""
          name: default-cert

Здесь важно сразу несколько вещей:

  • protocol: TLS, а не HTTPS — это принципиально

  • tls.mode: Passthrough явно говорит контроллеру: не трогай сертификаты

  • никаких certificateRefs — Gateway их просто не использует

Теперь можно описать сам маршрут:

apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
  name: tls-app-route
  namespace: app
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
  hostnames:
    - secure.example.com
  rules:
    - backendRefs:
        - name: tls-app-service
          port: 443

Что здесь происходит:

  • hostnames используются как SNI, а не как HTTP Host header

  • backend получает оригинальный TLS-трафик, включая client hello

В итоге архитектура становится такой:

  • Gateway — точка входа и SNI-маршрутизация

  • TLSRoute — описание правил для passthrough

  • Application — полностью отвечает за TLS, сертификаты и шифрование (например nginx или haproxy)

Без аннотаций, без а где же всё-таки терминируется TLS, и без сюрпризов при апгрейде контроллера. Редкий случай, когда Kubernetes-API реально упрощает жизнь, а не добавляет ещё один YAML ради YAML’а.

Маршрутизация gRPC-трафика

gRPC — ещё одна больная тема для Ingress. Формально он поддерживается, но на практике часто зависит от конкретного контроллера и набора флагов. В Gateway API gRPC — это просто частный случай HTTP/2, и для него есть нативная поддержка на уровне спецификации.

apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
  name: grpc-app-1
spec:
  parentRefs:
  - name: my-gateway
  hostnames:
  - "example.com"
  rules:
  - matches:
    - method:
        service: com.example.User
        method: Login
    backendRefs:
    - name: my-service1
      port: 50051
  - matches:
    - headers:
      - type: Exact
        name: magic
        value: foo
      method:
        service: com.example.Things
        method: DoThing
    backendRefs:
    - name: my-service2
      port: 50051

При желании можно матчить и по конкретным gRPC-сервисам и методам — в зависимости от возможностей контроллера. Важно, что сама модель маршрутизации остаётся стандартной и читаемой, без vendor-specific расширений.

В итоге даже на таком простом примере видно, зачем Gateway API понадобился: инфраструктура задаёт правила игры, приложения подключаются к ним прозрачным образом, а маршрутизация перестаёт быть общей болью всего кластера. И да, YAML стал длиннее — но, как показывает практика, читать его теперь приходится реже.

Миграции с NGINX Ingress на Envoy Gateway API

На практике миграция с NGINX Ingress на Envoy Gateway почти никогда не выглядит, как удалили Ingress — применили Gateway. Более реалистичный сценарий — аккуратный параллельный запуск с постепенным переносом трафика. Это особенно важно, если текущие Ingress уже обросли аннотациями и неочевидным поведением.

Типичный Ingress для NGINX может выглядеть примерно так: один объект, в котором смешаны хосты, пути, TLS и специфичные аннотации контроллера.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  namespace: app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80

Первый шаг миграции — оставить NGINX Ingress в покое и поднять Envoy Gateway параллельно. Мы создаём GatewayClass и Gateway, которые будут обслуживать новый входной контур, не влияя на существующий трафик. На этом этапе DNS всё ещё указывает на старый ingress-контроллер.

Дальше Ingress логически раскладывается на части. TLS и listener’ы переезжают в Gateway, а сама маршрутизация — в HTTPRoute.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: envoy-gateway
  namespace: gateway-system
spec:
  gatewayClassName: envoy
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: tls-cert
      allowedRoutes:
        namespaces:
          from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-http-redirect
  namespace: app
spec:
  parentRefs:
    - name: envoy-gateway
      namespace: gateway-system
      sectionName: http
  hostnames:
    - app.example.com
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            port: 443
            statusCode: 301
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-https-route
  namespace: app
spec:
  parentRefs:
    - name: envoy-gateway
      namespace: gateway-system
      sectionName: https
  hostnames:
    - app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /
      backendRefs:
        - name: api-service
          port: 8080

На этом этапе конфигурация становится заметно более многословной, но и более предсказуемой: TLS терминируется там, где это явно указано, маршрутизация живёт рядом с приложением, а поведение больше не зависит от набора аннотаций.

Финальный шаг — переключение трафика. Обычно это делается через DNS или балансировщик перед кластером: сначала небольшая доля запросов уходит на Envoy Gateway, затем — весь трафик. Старый Ingress остаётся как fallback, пока новый контур не покажет себя стабильно.

В результате миграция превращается не в рискованную операцию одним kubectl apply, а в управляемый процесс. NGINX Ingress уходит, Envoy Gateway начинает обслуживать трафик в рамках Gateway API, а сетевой слой кластера перестаёт быть местом, куда страшно заходить без крайней необходимости.

Типовые грабли и нюансы при работе с Gateway API

Первое, на что почти все натыкаются при переходе на Gateway API, — маршрут есть, а трафика нет. HTTPRoute корректный, статус зелёный, сервис живой, но запросы упорно не доходят. В большинстве случаев п��ичина банальна: маршрут не привязан к Gateway. В отличие от Ingress, здесь ничего не подхватывается автоматически — без корректного parentRefs и разрешений со стороны Gateway маршрут просто не будет обслуживаться. Это непривычно, но именно так спецификация защищает инфраструктуру от случайных изменений.

Вторая классическая ловушка — RBAC и cross-namespace routing. Gateway API активно поощряет сценарии, где Gateway живёт в одном namespace, а маршруты — в других. Но без явного разрешения через allowedRoutes такие маршруты будут молча игнорироваться. Здесь же приходится явно описывать, кому и что можно подключать. Зато потом не возникает вопросов, почему соседняя команда внезапно повесила свой сервис на ваш домен.

Отдельный пласт боли — TLS. Gateway API чётко разделяет TLS termination и TLS passthrough, но на этапе миграции это легко перепутать. Например, попытаться использовать TLSRoute там, где нужен HTTPRoute с HTTPS listener’ом, или положить сертификат не в тот namespace. В итоге всё выглядит корректно, но handshake не происходит. Рецепт простой: сначала решить, где именно должен терминироваться TLS, и только потом выбирать тип Route.

Четвёртый нюанс — диагностика и логи. Если раньше всё сводилось к логам ingress-nginx, то теперь у вас появляется несколько уровней: сам Gateway, контроллер (Envoy Gateway) и Envoy-прокси как data plane. Без понимания, где смотреть статус (kubectl describe gateway, kubectl get httproute -o yaml), дебаг быстро превращается в археологию. Хорошая новость — статус-объекты Gateway API обычно честно пишут, почему маршрут не принят.

И, наконец, человеческий фактор. Gateway API делает конфигурацию более явной, но и более формальной. YAML становится длиннее, объектов — больше, а быстро подкрутить аннотацию больше нельзя. Это раздражает ровно до первого инцидента, после которого внезапно выясняется, что читать и сопровождать такую конфигурацию всё-таки проще. Как обычно в Kubernetes: сначала непривычно, потом неудобно возвращаться назад.

Когда не стоит мигрировать прямо сейчас

Несмотря на все плюсы Gateway API, есть сценарии, где миграция объективно не даст выигрыша и только добавит сложности. Самый очевидный случай — маленький кластер с одним ingress-контроллером, парой сервисов и минимальными требованиями к маршрутизации. Если Ingress описывается одним YAML без аннотационного зоопарка и меняется раз в полгода, Gateway API в таком окружении будет выглядеть как инженерный оверкилл.

Второй важный стоп-фактор — отсутствие зрелой поддержки в вашем контроллере. Формально Gateway API — стандарт, но на практике уровень реализации сильно отличается. Где-то не поддерживается TLS passthrough, где-то частично работает gRPC, а где-то status-объекты ещё больше путают, чем помогают. Если текущий ingress-контроллер стабилен, хорошо изучен и закрывает все требования, имеет смысл подождать, пока поддержка Gateway API в вашем стеке дойдёт до нужного уровня.

Ещё один сигнал не сейчас — отсутствие организационной потребности. Gateway API особенно хорошо раскрывается в multi-team и multi-tenant кластерах, где важно разделение ответственности. Если же кластером и сетевым контуром управляет одна команда, а доступ к Ingress никто не ограничивает, архитектурные преимущества просто не будут использованы. Вы получите больше YAML, но не больше порядка.

Также стоит быть осторожным, если у вас сложная и критичная конфигурация Ingress, завязанная на специфичные возможности NGINX: кастомные snippets, нестандартные rewrite’ы, хитрые комбинации аннотаций. Перенос таких сценариев на Gateway API может потребовать переработки логики, а не прямого перевода. В таких случаях разумнее сначала вынести проблемные места, а уже потом думать о миграции.

И наконец, если у вас нет времени и ресурсов на спокойную обкатку. Gateway API — это не эксперимент, но и не кнопка сделать лучше. Он требует тестирования, наблюдаемости и понимания модели. Если сейчас приоритет — стабильность любой ценой, а Ingress эту стабильность обеспечивает, иногда лучший архитектурный выбор — ничего не трогать и вернуться к вопросу позже, уже с холодной головой и свободным окном в бэклоге.

Итоги

Ingress не исчезнет внезапно и не перестанет работать завтра — и это важно понимать. Он по-прежнему остаётся допустимым решением для множества кластеров и ещё долго будет жить (надеемся). Но при этом он уже достиг своего архитектурного потолка: модель не развивается, новые сценарии в неё не укладываются, а сложность продолжает расти за счёт аннотаций и неявных договорённостей.

Gateway API — это не Ingress, но лучше, а более зрелый взгляд на сетевой слой Kubernetes. Он вводит чёткое разделение ответственности, явные связи между объектами и расширяемую модель, которая масштабируется вместе с кластером и командами. Да, конфигурации стало больше. Зато стало меньше сюрпризов, меньше неочевидного поведения и меньше ситуаций, когда один YAML может случайно поломать половину кластера.

Миграция на Gateway API — это не обязательный шаг прямо сейчас, а осознанная инвестиция. Она особенно оправдана там, где Kubernetes уже перестал быть игрушкой: несколько команд, общий входной контур, требования к безопасности и управляемости. В таких условиях Gateway API перестаёт быть модным словом и становится инструментом, который реально упрощает жизнь.

Если резюмировать коротко: Ingress — это прошлое, с которым ещё можно жить. Gateway API — будущее, к которому стоит готовиться заранее. И чем раньше вы начнёте его хотя бы пробовать в тестовом кластере, тем меньше неожиданностей будет в тот момент, когда пора» превратится в «надо было вчера.