Проблема владения Gateway и как её решает ListenerSet
С уходом ingress-nginx я часто наблюдаю разочарование при миграции на Gateway API — из-за различий в моделях ресурсов.
Типичная проблема состоит в следующем: команды, использующие Ingress, нередко работают в модели самообслуживания (self-service), когда прикладные команды полностью владеют своей конфигурацией входящего трафика, включая TLS-сертификаты.
Выглядит это примерно так:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: my-issuer
name: app-a-routes
namespace: app-a
spec:
rules:
- host: app-a.example.com
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: myservice
port:
number: 80
tls:
- hosts:
- app-a.example.com
secretName: myingress-cert
В связке с cert-manager сертификат для app-a.example.com выпускался автоматически и подключался к общему экземпляру Nginx. DNS при этом обслуживался либо через wildcard-записи, либо через external-dns.
Проблема владения Gateway
В Gateway API можно сделать примерно то же самое, но с тонким, однако принципиальным отличием.
В Gateway API модель ресурсов разделена: есть Gateway и есть Routes. TLS настраивается именно в Gateway.
Выглядит это так:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: gateway
annotations:
cert-manager.io/cluster-issuer: my-issuer
spec:
gatewayClassName: my-gateway-class
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: app-a.example.com
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: myingress-cert
# Новая возможность ограничения области.
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: app-a
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-a-routes
namespace: app-a
spec:
parentRefs:
- name: my-gateway
namespace: gateway
hostnames:
- app-a.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: myservice
port: 80
Ключевое изменение здесь: TLS-конфигурация для app-a переехала из пространства имён app-a в общее пространство имён шлюза. Прикладные команды больше не могут обслуживать себя самостоятельно!
Помимо этого, даже без требования мультиарендности (multi-tenancy), Gateway поддерживает не более 16 слушателей (listeners). Это означает, что при наличии хотя бы нескольких десятков доменов вы вынуждены использовать wildcard-сертификаты.
Знакомьтесь: ListenerSet
ListenerSet — это новое решение обеих проблем, вошедшее в статус GA в Gateway API v1.5, выпущенном в феврале 2026 года как раз перед уходом ingress-nginx.
Идея ListenerSet заключается в том, чтобы вынести listeners за пределы Gateway, устранив оба недостатка сразу:
-
Теперь можно иметь неограниченное количество слушателей (если реализация это поддерживает!), каждый — со своим доменом.
-
Теперь
GatewayиListenerSetмогут находиться в разных пространствах имён, что возвращает возможность самообслуживания.
Что не возвращается — так это простота единого ресурса, как у Ingress (скорее наоборот: теперь ресурсов три), но что есть, то есть.
Проверяем на практике
Поскольку ListenerSet — вещь довольно новая, мне удалось найти только 2 реализации с поддержкой — Kgateway и Agentgateway. Подбросив монету, я остановился на Agentgateway для демонстрации.
Попробуем!
Сначала установим всё необходимое:
$ kubectl kustomize https://github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.5.1 | kubectl apply --server-side -f -
$ helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--namespace cert-manager \
--create-namespace \
--set config.enableGatewayAPI=true \
--set config.enableGatewayAPIListenerSet=true \
--set config.featureGates.ListenerSets=true \
--set crds.enabled=true
$ helm upgrade -i --create-namespace --namespace agentgateway-system agentgateway-crds --version 1.0.1 oci://cr.agentgateway.dev/charts/agentgateway-crds
$ helm upgrade -i --namespace agentgateway-system agentgateway --version 1.0.1 oci://cr.agentgateway.dev/charts/agentgateway
Теперь возьмём пример выше, но вынесем слушатель в ListenerSet в пространстве имён приложения:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <redacted>
privateKeySecretRef:
name: letsencrypt-acc-key
solvers:
- http01:
gatewayHTTPRoute: {}
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: gateway
spec:
gatewayClassName: agentgateway
allowedListeners:
namespaces:
from: All
listeners:
# Пока нет способа задать нулевое количество слушателей :-(
- name: http
hostname: dummy
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
name: app-a
namespace: app-a
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
parentRef:
name: my-gateway
namespace: gateway
listeners:
- name: https
hostname: app-a.k8s.howardjohn.info
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: backend-tls
- name: http
hostname: app-a.k8s.howardjohn.info
port: 80
protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-a-routes
namespace: app-a
spec:
parentRefs:
- kind: ListenerSet
name: app-a
sectionName: https
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: echo
port: 80
Подождав немного, пока выпустится сертификат, обращаемся к нашему новому сервису:
$ curl https://app-a.k8s.howardjohn.info
ServiceVersion=
ServicePort=80
Host=app-a.k8s.howardjohn.info
URL=/
Method=GET
Proto=HTTP/1.1
IP=10.34.131.5
RequestHeader=Accept:*/*
RequestHeader=User-Agent:curl/8.19.0
Hostname=echo-8fbd95d6d-fk2kt
Добавить ещё одно приложение — то же самое, только разворачиваем в новом пространстве имён:
apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
name: app-b
namespace: app-b
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
parentRef:
name: my-gateway
namespace: gateway
listeners:
- name: https
hostname: app-b.k8s.howardjohn.info
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: backend-tls
- name: http
hostname: app-b.k8s.howardjohn.info
port: 80
protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-b-routes
namespace: app-b
spec:
parentRefs:
- kind: ListenerSet
name: app-b
sectionName: https
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: echo
port: 80
Проверяем:
$ curl https://app-b.k8s.howardjohn.info
ServiceVersion=
ServicePort=80
Host=app-b.k8s.howardjohn.info
URL=/
Method=GET
Proto=HTTP/1.1
IP=10.34.130.12
RequestHeader=Accept:*/*
RequestHeader=User-Agent:curl/8.19.0
Hostname=echo-8fbd95d6d-b54w8
Отлично! Теперь мы можем самостоятельно получать TLS-сертификаты через Gateway API.
Миграция
Здесь я показал лишь основы работы с новым ресурсом ListenerSet в Gateway. Если вы мигрируете с Ingress на Gateway API, рекомендую:
-
Использовать инструмент ingress2gateway для автоматизации миграции.
-
Заглянуть на страницу реализаций и в Gateway API Benchmarks, чтобы выбрать подходящую реализацию.