Внешний доступ к KServe

Раздел по ML-сценариям уточняется. Отдельные шаги и примеры могут быть дополнены. При возникновении вопросов обратитесь к команде платформы «Штурвал».

KServe входит в установку Kubeflow. Из Notebook в том же кластере модель доступна по внутреннему имени сервиса. Шаги этой страницы нужны, только если модель вызывают снаружи — другая система, CI, пользовательский сервис.

Снаружи адрес выглядит как: http://kserve.<commonName>/serving/<неймспейс-профиля>/<имя-модели>/v1/models/<имя-модели>:predict.

Переопределения спецификации сервиса задавайте только через ShturvalServicePatch (SSP). Не дописывайте customvalues в SSC kubeflow-step-1 и kubeflow-step-2, которые создавали при установке.

Что должно получиться

  1. Knative Serving знает домен кластера и не преобразует тег образа в digest для перечисленных реестров. Если CA реестра отсутствует в доверенном хранилище контроллера Knative, ему монтируется корневой CA реестра.
  2. KServe публикует модели по одному хосту и пути /serving/<неймспейс>/<имя>.
  3. Внешний nginx принимает kserve.<commonName>, подменяет Host на <commonName> и отдаёт трафик в istio-ingressgateway:80.
  4. AuthorizationPolicy разрешает пути inference. Снаружи запрос несёт cookie сессии Kubeflow. Сама политика ограничивает пути, но не проверяет cookie.

Требования

Ресурсы Kubernetes создавайте в клиентском кластере. Проверочный запрос на шаге 6 выполняйте с внешнего хоста, для которого настраивается доступ.

  • установлены kubeflow-step-0 … kubeflow-step-4, режим Авто, состояние Healthy;
  • известно <commonName> — status.appliedCustomValues.controller.shturval.commonName у SSC shturval-ingress-controller;
  • DNS kserve.<commonName> указывает на Ingress клиентского кластера (тот же, что у kubeflow.<commonName>);
  • есть Profile Kubeflow и его неймспейс. Не используйте default и kubeflow-system как рабочую область;
  • в реестре есть образ runtime модели (в примере — sklearnserver);
  • известен <хост-реестра> — FQDN реестра, откуда кластер тянет этот образ (часто FQDN зеркала).

Подставляйте свои значения:

Плейсхолдер Откуда взять
<commonName> SSC shturval-ingress-controller, controller.shturval.commonName
<хост-реестра> хост из URL образа runtime, без схемы и пути
<неймспейс-профиля> имя Profile / неймспейса команды
<тег> тег образа sklearnserver в вашем реестре
<URI-модели> URI артефакта модели (S3 или иной storage, который видит кластер)

Куда подставлять <commonName>:

Где Значение
Knative configDomain <commonName>
KServe ingressDomain <commonName>
Ingress host kserve.<commonName>
Ingress upstream-vhost <commonName>

В configDomain и ingressDomain не указывайте kserve.<commonName>: nginx как раз подменяет Host на голый <commonName>, и VirtualService KServe должен совпасть с этим Host.


Шаг 1. Корневой CA зеркала

Образы моделей кластер скачивает с зеркала. Соединение идёт по HTTPS, поэтому кластер должен доверять сертификату зеркала.

Если сертификат зеркала выпущен удостоверяющим центром, которому контроллер Knative не доверяет, подключите корневой CA зеркала.

Если зеркало и кластер уже используют один и тот же CA, шаг можно пропустить: не создавайте ConfigMap, а блок controller в SSP на шаге 2 удалите.

Запросите корневой сертификат у администратора зеркала. Для диагностики можно получить цепочку сертификатов командой ниже, подставив FQDN зеркала:

echo | openssl s_client -showcerts -servername <хост-зеркала> -connect <хост-зеркала>:443 2>/dev/null

Не определяйте корневой сертификат только по его положению в выводе: сервер может не передать корневой CA. Перед добавлением проверьте subject, issuer и отпечаток сертификата. Сохраните подтверждённый корневой сертификат в файл, например mirror-root-ca.pem.

Неймспейс knative-serving появляется после установки Kubeflow. Создайте в нём ConfigMap с сертификатом.

  1. Откройте клиентский кластер.
  2. Импортируйте ConfigMap. Вставьте текст сертификата вместо плейсхолдера:
apiVersion: v1
kind: ConfigMap
metadata:
  name: registry-root-ca
  namespace: knative-serving
data:
  ca-bundle.pem: |
    -----BEGIN CERTIFICATE-----
    <PEM-корневого-CA-зеркала>
    -----END CERTIFICATE-----    
  1. Неймспейс knative-serving → Config Maps: есть registry-root-ca, ключ ca-bundle.pem.
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" -n knative-serving create configmap registry-root-ca \
  --from-file=ca-bundle.pem=<путь>/mirror-root-ca.pem
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" -n knative-serving get configmap registry-root-ca
Параметры манифеста
Параметр Зачем нужен
name: registry-root-ca Имя ConfigMap. На него ссылается SSP на шаге 2 (controller.caBundle.configMapName).
namespace: knative-serving Неймспейс Knative Serving. Появляется после установки Kubeflow.
ключ ca-bundle.pem Корневой сертификат зеркала в PEM.

Шаг 2. SSP для Knative Serving

SSP задаёт домен кластера, список реестров без проверки тега и (если выполняли шаг 1) том с CA.

  1. Откройте клиентский кластер.
  2. Импортируйте SSP:
apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: knative-for-kserve-external
spec:
  patchOrder: 100
  shturvalServiceConfigName: kubeflow-step-1
  customvalues:
    knative-serving1:
      configDomain:
        domains:
          - "<commonName>": {}
      configDeployment:
        extraArgs:
          registries-skipping-tag-resolving: "r.shturval.tech,<хост-реестра>,kind.local,ko.local,dev.local"
      controller:
        caBundle:
          configMapName: registry-root-ca
          key: ca-bundle.pem
          path: ca-bundle.pem
          mountPath: /etc/ssl/custom-certs
  1. Установленные сервисы → kubeflow-step-1 → вкладка ShturvalServicePatch: есть knative-for-kserve-external. Дождитесь Healthy у сервиса.
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" apply -f - <<'EOF'
apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: knative-for-kserve-external
spec:
  patchOrder: 100
  shturvalServiceConfigName: kubeflow-step-1
  customvalues:
    knative-serving1:
      configDomain:
        domains:
          - "<commonName>": {}
      configDeployment:
        extraArgs:
          registries-skipping-tag-resolving: "r.shturval.tech,<хост-реестра>,kind.local,ko.local,dev.local"
      controller:
        caBundle:
          configMapName: registry-root-ca
          key: ca-bundle.pem
          path: ca-bundle.pem
          mountPath: /etc/ssl/custom-certs
EOF

Подставьте <commonName> и <хост-реестра> перед применением манифеста. Если ConfigMap не создавали, удалите блок controller из манифеста.

kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get shturvalservicepatch knative-for-kserve-external
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get ssc kubeflow-step-1
Параметры манифеста
Параметр Зачем нужен
shturvalServiceConfigName: kubeflow-step-1 SSP накладывается на SSC kubeflow-step-1, сам SSC при установке не правится.
configDomain.domains Knative принимает запросы на <commonName> после подмены Host.
registries-skipping-tag-resolving Knative не преобразует тег образа в digest для этих реестров. Образ по-прежнему скачивает среда выполнения контейнеров на узле. Оставьте r.shturval.tech и служебные kind.local,ko.local,dev.local, добавьте свой <хост-реестра>.
controller.caBundle Подключает ConfigMap registry-root-ca с CA зеркала. Уберите весь блок controller, если шаг 1 пропускали.

Шаг 3. SSP для KServe

SSP задаёт pathTemplate и ingressDomain, чтобы к разным моделям был доступ по одному хосту и разным путям. Подставьте <commonName> только в ingressDomain. Остальные поля оставьте как в примере: gateway Istio, класс istio, схема http, путь /serving/{{ "{{ .Namespace }}" }}/{{ "{{ .Name }}" }}.

  1. Откройте клиентский кластер.
  2. Импортируйте SSP:
apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: kserve-path-template
spec:
  patchOrder: 100
  shturvalServiceConfigName: kubeflow-step-2
  customvalues:
    kserve:
      inferenceserviceConfig:
        ingress: |-
          {
              "enableGatewayApi": false,
              "kserveIngressGateway": "istio-system/istio-ingressgateway",
              "ingressGateway" : "kubeflow/kubeflow-gateway",
              "localGateway" : "knative-serving/knative-local-gateway",
              "localGatewayService" : "knative-local-gateway.istio-system.svc.cluster.local",
              "ingressDomain"  : "<commonName>",
              "ingressClassName" : "istio",
              "domainTemplate": "{{ "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}" }}",
              "pathTemplate": "{{ "/serving/{{ .Namespace }}/{{ .Name }}" }}",
              "urlScheme": "http",
              "disableIstioVirtualHost": false,
              "disableIngressCreation": false
          }          
  1. Установленные сервисы → kubeflow-step-2 → вкладка ShturvalServicePatch: есть kserve-path-template. Дождитесь Healthy.
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" apply -f - <<'EOF'
apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: kserve-path-template
spec:
  patchOrder: 100
  shturvalServiceConfigName: kubeflow-step-2
  customvalues:
    kserve:
      inferenceserviceConfig:
        ingress: |-
          {
              "enableGatewayApi": false,
              "kserveIngressGateway": "istio-system/istio-ingressgateway",
              "ingressGateway" : "kubeflow/kubeflow-gateway",
              "localGateway" : "knative-serving/knative-local-gateway",
              "localGatewayService" : "knative-local-gateway.istio-system.svc.cluster.local",
              "ingressDomain"  : "<commonName>",
              "ingressClassName" : "istio",
              "domainTemplate": "{{ "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}" }}",
              "pathTemplate": "{{ "/serving/{{ .Namespace }}/{{ .Name }}" }}",
              "urlScheme": "http",
              "disableIstioVirtualHost": false,
              "disableIngressCreation": false
          }
EOF

Подставьте <commonName> до apply.

kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get shturvalservicepatch kserve-path-template
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get ssc kubeflow-step-2
Параметры манифеста
Параметр Зачем нужен
shturvalServiceConfigName: kubeflow-step-2 SSP накладывается на SSC kubeflow-step-2.
ingressDomain Домен кластера <commonName>. Не указывайте kserve.<commonName>.
pathTemplate Путь к модели: /serving/<неймспейс>/<имя>. По нему собирается внешний URL.
domainTemplate Шаблон хоста KServe. Оставьте как в примере.
urlScheme: http Внешний Ingress в примере без TLS.
ingressClassName: istio Внутри кластера трафик идёт через Istio.
kserveIngressGateway / ingressGateway / localGateway Шлюзы Istio и Knative. Оставьте как в примере.
enableGatewayApi: false Gateway API не используется.
disableIstioVirtualHost / disableIngressCreation VirtualService KServe создаётся. Оставьте false.

Шаг 4. Ingress и AuthorizationPolicy

Два объекта в неймспейсе istio-system:

  • Ingress kserve-external — внешняя точка входа nginx на хост kserve.<commonName>;
  • AuthorizationPolicy kserve-inference-access — разрешает на подах модели служебные пути inference.

В примере внешний Ingress без TLS, схема http. UI Kubeflow при этом остаётся на https://kubeflow.<commonName>/.

Обратите внимание! Используйте этот HTTP-вариант только в изолированном тестовом контуре. Не передавайте cookie рабочей сессии по недоверенной сети. Для промышленной эксплуатации настройте TLS и проверку аутентификации на внешней точке входа.
  1. Откройте клиентский кластер.
  2. Замените <commonName> в манифесте и загрузите оба объекта одним файлом: импорт манифестов → вставьте YAML → Проверить → Сохранить.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kserve-external
  namespace: istio-system
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/upstream-vhost: "<commonName>"
spec:
  ingressClassName: nginx
  rules:
  - host: kserve.<commonName>
    http:
      paths:
      - path: /serving/
        pathType: Prefix
        backend:
          service:
            name: istio-ingressgateway
            port:
              number: 80
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: kserve-inference-access
  namespace: istio-system
  labels:
    app.kubernetes.io/part-of: kserve-integration
spec:
  action: ALLOW
  rules:
  - to:
    - operation:
        paths:
        - /v1/models/*
        - /v2/models/*
        - /v2/health/*
  1. Неймспейс istio-system → Сеть → Ingresses: есть Ingress kserve-external, хост kserve.<commonName>, путь /serving/.
  2. Администрирование → Кастомные ресурсы → группа security.istio.io → AuthorizationPolicy → kserve-inference-access в неймспейсе istio-system.

Подставьте <commonName> и выполните:

kubectl --kubeconfig "${KUBECONFIG_CLIENT}" apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kserve-external
  namespace: istio-system
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/upstream-vhost: "<commonName>"
spec:
  ingressClassName: nginx
  rules:
  - host: kserve.<commonName>
    http:
      paths:
      - path: /serving/
        pathType: Prefix
        backend:
          service:
            name: istio-ingressgateway
            port:
              number: 80
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: kserve-inference-access
  namespace: istio-system
  labels:
    app.kubernetes.io/part-of: kserve-integration
spec:
  action: ALLOW
  rules:
  - to:
    - operation:
        paths:
        - /v1/models/*
        - /v2/models/*
        - /v2/health/*
EOF
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get ingress kserve-external -n istio-system
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get authorizationpolicy kserve-inference-access -n istio-system
Параметры манифестов
Параметр Ресурс Зачем нужен
host: kserve.<commonName> Ingress Имя, которое указывают снаружи. DNS kserve.<commonName> должен указывать на Ingress клиентского кластера.
path: /serving/ Ingress KServe в Serverless ищет модель по пути /serving/<неймспейс>/<имя>. Срезать префикс нельзя — иначе модель не найдётся.
backend.service: istio-ingressgateway:80 Ingress nginx не ходит к поду модели напрямую: сначала Istio, уже оттуда — к KServe.
upstream-vhost: "<commonName>" Ingress Подменяет Host kserve.<commonName> на голый <commonName>, чтобы сработал VirtualService, который создаёт сам KServe.
proxy-read-timeout / proxy-send-timeout: 3600 Ingress Длинный ответ модели (в том числе потоковая выдача) не обрывается через минуту.
ingressClassName: nginx Ingress Заявку обрабатывает shturval-ingress-controller, тот же класс, что у Kubeflow.
action: ALLOW и пути /v1/models/*, /v2/models/*, /v2/health/* AuthorizationPolicy Предиктор принимает predict/explain и health. Политика в istio-system действует на весь mesh.
Источник (from.source) не задан AuthorizationPolicy Activator Knative ходит к предиктору без mTLS. Правило с проверкой источника такой запрос бы отбросило. Эта политика не проверяет cookie и сама по себе не аутентифицирует внешний запрос.

Шаг 5. Тестовая модель

Создайте InferenceService в неймспейсе Profile. minReplicas: 1 — чтобы ревизия не ушла в ноль до проверки. Образ runtime возьмите из своего реестра.

Укажите <URI-модели> — адрес совместимого артефакта модели в доступном кластеру хранилище. Образ среды выполнения задаётся отдельно в поле image.

  1. Откройте клиентский кластер.
  2. Импортируйте манифест. Подставьте неймспейс, образ и URI:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: sklearn-iris
  namespace: <неймспейс-профиля>
spec:
  predictor:
    minReplicas: 1
    sklearn:
      image: <хост-реестра>/kubeflow/kserve/sklearnserver:<тег>
      storageUri: "<URI-модели>"
      resources:
        requests:
          cpu: 50m
          memory: 128Mi
        limits:
          cpu: 200m
          memory: 256Mi
  1. Администрирование → Кастомные ресурсы → группа serving.kserve.io → InferenceService: у sklearn-iris состояние Ready.
kubectl --kubeconfig "${KUBECONFIG_CLIENT}" apply -f - <<'EOF'
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: sklearn-iris
  namespace: <неймспейс-профиля>
spec:
  predictor:
    minReplicas: 1
    sklearn:
      image: <хост-реестра>/kubeflow/kserve/sklearnserver:<тег>
      storageUri: "<URI-модели>"
      resources:
        requests:
          cpu: 50m
          memory: 128Mi
        limits:
          cpu: 200m
          memory: 256Mi
EOF

Дождитесь Ready:

kubectl --kubeconfig "${KUBECONFIG_CLIENT}" get inferenceservice sklearn-iris -n <неймспейс-профиля> -w

Статус True / Ready означает, что модель поднята внутри кластера. Это ещё не проверка внешнего HTTP.

Если ревизия не переходит в состояние Ready, проверьте события пода предиктора: ошибки ImagePull, проверки сертификата x509 и доступа к хранилищу. Сверьте <хост-реестра> со списком registries-skipping-tag-resolving и проверьте наличие ConfigMap с CA, если контроллер Knative не доверяет CA реестра.

Параметры манифеста
Параметр Зачем нужен
name: sklearn-iris Имя модели. Входит во внешний путь /serving/…/sklearn-iris/….
namespace Неймспейс Profile, не default.
minReplicas: 1 Ревизия не уходит в ноль до проверки.
image Образ runtime из зеркала.
storageUri Откуда runtime читает артефакт модели.
resources Запросы и лимиты CPU и памяти для тестового предиктора.

Шаг 6. Проверьте внешний вызов

Запрос к модели — это POST, а не открытие адреса в браузере. Cookie сессии выдаёт oauth2-proxy после входа в UI Kubeflow.

Обратите внимание! Значение cookie — секрет. Не вставляйте его в документацию, тикеты и скриншоты. Считайте cookie секретом до истечения срока действия или отзыва сессии. Команду ниже выполняйте только в изолированном тестовом контуре.
  1. В браузере откройте https://kubeflow.<commonName>/ и войдите.
  2. Откройте инструменты разработчика → Cookies (Chrome: Application → Cookies, Firefox: Storage → Cookies) для хоста kubeflow.<commonName>.
  3. Скопируйте значение cookie oauth2_proxy_kubeflow.
export COOKIE='oauth2_proxy_kubeflow=<значение-cookie>'

curl -sS -D - -X POST \
  -H "Cookie: ${COOKIE}" \
  -H 'Content-Type: application/json' \
  -d '{"instances": [[6.8, 2.8, 4.8, 1.4]]}' \
  "http://kserve.<commonName>/serving/<неймспейс-профиля>/sklearn-iris/v1/models/sklearn-iris:predict"

Ожидается HTTP 200 и поле predictions в теле ответа. Значение зависит от модели, указанной в <URI-модели>.

Если ответа нет:

Симптом Что проверить
Нет маршрута, таймаут DNS kserve.<commonName>, Ingress kserve-external
404 pathTemplate, путь /serving/<неймспейс>/<имя>/v1/models/<имя>:predict, upstream-vhost равен <commonName>
401 / 403 cookie oauth2_proxy_kubeflow, AuthorizationPolicy
503, пустой предиктор InferenceService ещё не Ready, minReplicas

Связанные страницы: