ListenerSet в Gateway API: самообслуживание TLS вернулось

Проблема владения 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, рекомендую:

© 2026 meganuke