
Выход в продакшен с собственными языковыми моделями внутри корпоративного контура часто упирается в высокую стоимость GPU-оборудования и сложные риски. Внешние сервисы вроде ChatGPT не подходят из-за требований к безопасности данных, а аренда или покупка физических серверов может приводить к переплатам за простой в неактивные часы или, наоборот, падению сервиса при пиковых нагрузках. Помимо самой модели, бизнесу необходимо развертывать и связывать между собой векторные базы данных, сервисы векторизации и оркестраторы сценариев.
Эту проблему решает запуск приватной LLM в облачном Managed Kubernetes. Приватная модель гарантирует конфиденциальность и не отправляет данные во внешние сети, облако переводит капитальные затраты в гибкие операционные, а Kubernetes берет на себя отказоустойчивость и автоматическое масштабирование дорогих GPU-ресурсов. Материал будет полезен DevOps-, MLOps-инженерам и архитекторам, перед которыми стоит задача развернуть изолированную и надежную RAG-систему.
В этой статье рассмотрим, как можно задеплоить LLM в Managed Kubernetes на примере простой RAG-системы. Будем использовать LLM-роутер AIBrix, n8n и vLLM. Дополнительно понадобятся Envoy Gateway (как зависимость для AIBrix), cert-manager (выпустим сертификаты для домена n8n), Qdrant (хранилище для RAG системы) и PostgreSQL в качестве базы данных для n8n.
Здесь мы не будем рассматривать деплой системы мониторинга. Подробную информацию о ее настройке вы можете найти в нашем репозитории.
Создание кластера
Развертывать кластер Managed Kubernetes будем на облачных серверах в регионе ru-6 — там доступен большой выбор конфигураций с GPU.
В кластере желательно иметь дополнительные ноды для критических системных компонентов из неймспейса kube-system и AIBrix-компонентов. Я оставил две ноды по 4 vCPU, 8 ГБ RAM и диск на 50 ГБ, но можно использовать и менее производительные конфигурации. При такой схеме GPU-ноды при необходимости можно масштабировать в ноль, и все компоненты, включая AIBrix, продолжат работу.
Для работы с GPU я использую две ноды со следующими характеристиками: 8 vCPU, 32 ГБ RAM, диск на 150 ГБ и 1 × GPU RTX 4090 24 ГБ. Диск на 150 ГБ выбран с запасом, чтобы веса модели гарантированно поместились в эфемерном хранилище Kubernetes. При необходимости можно взять больше или меньше. Модели GPU также можно выбрать другие.
При создании группы GPU-нод стоит установить флаг для автоматической установки GPU-драйверов и nvidia-device-plugin. Процессу vLLM мы будем выделять GPU целиком.
Для группы GPU-нод установим taint при создании: vllm = true : NoExecute. Это нужно, чтобы на этой ноде запускались только поды vllm, поскольку они требуют большого объема вычислительных ресурсов. Нода должна оставаться свободной, иначе поды vLLM зависнут в состоянии Pending.
AIBrix
Изначально планировалось провернуть все на AIBrix версии v0.5.0, но к моменту написания статьи уже вышел релиз v0.7.0. Вот наиболее важные изменения:
Добавлена поддержка аудиоинтерфейсов в стиле OpenAI (
/v1/audio/transcriptions, /v1/audio/translations) вместе с новым API вывода/v1/classify.Введены совместимые с OpenAI API генерации изображений и видео (
/v1/images/generations, /v1/video/generations), позволяющие запускать многомодальные рабочие нагрузки генерации через один и тот же шлюз.Добавлена поддержка моделей переранжирования через эндпоинт
/v1/rerankдля улучшения конвейеров.Реализован Wire-compatible Batch API для асинхронной пакетной обработки эндпоинтов
/v1/chat/completions,/v1/completionsи/v1/embeddings, поддерживаемый постоянным хранилищем метаданных и асинхронным конечным автоматом заданий.Запущена AIBrix Management Console (Preview) — новый фронтенд на React и бэкенд на Go, позволяющие регистрировать модели, развертывать приложения из версионированных шаблонов, отслеживать пакетные задания и загружать результаты без необходимости использования kubectl/YAML.
Проект активно развивается и, думаю, хорошо себя покажет на дистанции.
Перейдем к установке. Развернем базовые стабильные компоненты согласно официальному руководству по быстрому старту (Quickstart) в README.md проекта, однако внесем в них несколько патчей для оптимизации работы.
Создаем файл kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/vllm-project/aibrix/releases/download/v0.7.0/aibrix-core-v0.7.0.yaml patches: - target: group: gateway.envoyproxy.io version: v1alpha1 kind: EnvoyProxy name: aibrix-custom-proxy-config namespace: aibrix-system patch: |- - op: move from: /spec/provider/kubernetes/envoyDeployment path: /spec/provider/kubernetes/envoyDaemonSet - op: remove path: /spec/provider/kubernetes/envoyDaemonSet/replicas - op: remove path: /spec/provider/kubernetes/envoyDaemonSet/strategy - op: replace path: /spec/provider/kubernetes/envoyDaemonSet/patch/value/spec/template/spec/containers/0/resources value: requests: cpu: "1" memory: 2Gi limits: cpu: "1" memory: 2Gi - op: add path: /spec/provider/kubernetes/envoyService value: externalTrafficPolicy: Cluster - target: group: apps version: v1 kind: Deployment name: aibrix-gateway-plugins namespace: aibrix-system patch: |- - op: replace path: /spec/template/spec/containers/0/resources value: requests: cpu: "1" memory: 2Gi limits: cpu: "1" memory: 2Gi - target: group: gateway.networking.k8s.io version: v1 kind: Gateway name: aibrix-eg namespace: aibrix-system patch: |- - op: add path: /spec/infrastructure value: annotations: service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
У нас есть три патча, которые выполняют следующие задачи:
Перевод
EnvoyProxyизDeploymentвDaemonSetи включение параметраexternalTrafficPolicy: Clusterдля корректной работы с балансировщиком нагрузки по умолчанию. Если вы хотите распределять трафик только по определенным нодам, добавьте аннотацию node-selector для openstack-ccm и перечислите фильтры.Уменьшение объема выделяемых ресурсов на один инстанс для балансировщика с дефолтных 2 vCPU и 8 ГБ RAM до 1 vCPU и 2 ГБ RAM.
Оптимизация запрашиваемых ресурсов (
resource requests) для компонентаaibrix-gateway-pluginsс помощью отдельного патча.Добавление аннотации для балансировщика, чтобы зафиксировать за ним только приватный IP-адрес. Этот шаг не является обязательным: вы можете оставить публичный эндпоинт для обращений к моделям из внешней сети, но в таком случае обязательно настройте авторизацию. В этой статье мы опубликуем во внешнюю сеть только интерфейс n8n.
Применяем манифесты (в директории, где создали kustomization.yaml):
kubectl apply --server-side -f \ "https://github.com/vllm-project/aibrix/releases/download/v0.7.0/aibrix-dependency-v0.7.0.yaml" kubectl apply --server-side -f \ "https://github.com/vllm-project/aibrix/releases/download/v0.7.0/aibrix-core-crds-v0.7.0.yaml" kubectl apply -k .
vLLM
Создадим токен, которым будем авторизовываться при запросах в LLM и запишем его в секрет:
kubectl create secret generic vllm-api-key \ --from-literal=api-key=$(pwgen -s1 50)
Для автоматической генерации токена использую конструкцию $(pwgen -s1 50). Впрочем, без этого можно обойтись и задать пароль текстом.

Managed Kubernetes на выделенных серверах
Снизьте расходы на ИТ‑инфраструктуру и улучшите производительность микросервисов.
Манифесты для деплоя моделей
Ниже представлены манифесты Kubernetes для запуска моделей через vLLM. Мы развернем два компонента:
Instruct-модель (qwen3-8b) — отвечает за генерацию текста и логику работы агента.
Embedding-модель (qwen3-embedding-8b) — отвечает за векторизацию текста для RAG-системы.
Каждый Deployment создает под с одной копией vLLM, которому выделяется ровно один GPU целиком.
Instruct
# Qwen3-8B. # # GPU allocation: one exclusive RTX 4090 24 GB. # vLLM may use up to 90% of its GPU memory; the remaining 10% is reserved for # CUDA contexts and transient allocations. # # Optional API-key Secret: # kubectl create secret generic vllm-api-key \ # --from-literal=api-key='<value>' apiVersion: apps/v1 kind: Deployment metadata: name: qwen3-8b namespace: default labels: model.aibrix.ai/name: qwen3-8b model.aibrix.ai/port: "8000" spec: replicas: 1 # A rolling update could temporarily start a second copy and exhaust the # shared GPU. Recreate keeps the VRAM budget deterministic. strategy: type: Recreate selector: matchLabels: model.aibrix.ai/name: qwen3-8b template: metadata: labels: model.aibrix.ai/name: qwen3-8b model.aibrix.ai/port: "8000" annotations: prometheus.io/path: /metrics prometheus.io/port: "8000" prometheus.io/scrape: "true" spec: terminationGracePeriodSeconds: 30 containers: - name: vllm-openai image: vllm/vllm-openai:v0.15.1 imagePullPolicy: IfNotPresent command: - vllm - serve args: - Qwen/Qwen3-8B - --tokenizer - Qwen/Qwen3-8B - --hf-config-path - Qwen/Qwen3-8B - --served-model-name - qwen3-8b - --host - 0.0.0.0 - --port - "8000" - --uvicorn-log-level - warning - --dtype - half - --max-model-len - "16384" - --max-num-seqs - "4" - --gpu-memory-utilization - "0.90" - --enable-prefix-caching - --enforce-eager - --enable-auto-tool-choice - --tool-call-parser - "hermes" env: - name: VLLM_API_KEY valueFrom: secretKeyRef: name: vllm-api-key key: api-key optional: true - name: HF_HOME value: /cache/huggingface ports: - name: serve containerPort: 8000 protocol: TCP resources: requests: cpu: "2" memory: 12Gi nvidia.com/gpu: "1" limits: cpu: "2" memory: 12Gi nvidia.com/gpu: "1" livenessProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 3 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 readinessProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 startupProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 30 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 volumeMounts: - name: model-cache mountPath: /cache/huggingface - name: dshm mountPath: /dev/shm volumes: - name: model-cache emptyDir: {} - name: dshm emptyDir: medium: Memory sizeLimit: 2Gi tolerations: - key: vllm operator: Exists effect: NoExecute --- apiVersion: v1 kind: Service metadata: name: qwen3-8b namespace: default labels: model.aibrix.ai/name: qwen3-8b prometheus-discovery: "true" annotations: prometheus.io/path: /metrics prometheus.io/port: "8000" prometheus.io/scrape: "true" spec: type: ClusterIP selector: model.aibrix.ai/name: qwen3-8b ports: - name: serve port: 8000 targetPort: serve protocol: TCP
embedding
# Qwen3-Embedding-8B in BF16. # # GPU allocation: one exclusive RTX 4090 24 GB. # vLLM may use up to 90% of its GPU memory; the remaining 10% is reserved for # CUDA contexts and transient allocations. # # Optional API-key Secret: # kubectl create secret generic vllm-api-key \ # --from-literal=api-key='<value>' apiVersion: apps/v1 kind: Deployment metadata: name: qwen3-embedding-8b namespace: default labels: model.aibrix.ai/name: qwen3-embedding-8b model.aibrix.ai/port: "8000" spec: replicas: 1 strategy: type: Recreate selector: matchLabels: model.aibrix.ai/name: qwen3-embedding-8b template: metadata: labels: model.aibrix.ai/name: qwen3-embedding-8b model.aibrix.ai/port: "8000" annotations: prometheus.io/path: /metrics prometheus.io/port: "8000" prometheus.io/scrape: "true" spec: terminationGracePeriodSeconds: 30 containers: - name: vllm-openai image: vllm/vllm-openai:v0.15.1 imagePullPolicy: IfNotPresent command: - vllm - serve args: - Qwen/Qwen3-Embedding-8B - --served-model-name - qwen3-embedding-8b - --host - "0.0.0.0" - --port - "8000" - --uvicorn-log-level - warning - --runner - pooling - --dtype - bfloat16 - --max-model-len - "32768" - --max-num-seqs - "8" - --gpu-memory-utilization - "0.90" env: - name: VLLM_API_KEY valueFrom: secretKeyRef: name: vllm-api-key key: api-key optional: true - name: HF_HOME value: /cache/huggingface ports: - name: serve containerPort: 8000 protocol: TCP resources: requests: cpu: "2" memory: 12Gi nvidia.com/gpu: "1" limits: cpu: "2" memory: 12Gi nvidia.com/gpu: "1" livenessProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 3 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 readinessProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 startupProbe: httpGet: path: /health port: 8000 scheme: HTTP failureThreshold: 30 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 volumeMounts: - name: model-cache mountPath: /cache/huggingface - name: dshm mountPath: /dev/shm volumes: - name: model-cache emptyDir: {} - name: dshm emptyDir: medium: Memory sizeLimit: 1Gi tolerations: - key: "vllm" operator: "Exists" effect: NoExecute --- apiVersion: v1 kind: Service metadata: name: qwen3-embedding-8b namespace: default labels: model.aibrix.ai/name: qwen3-embedding-8b prometheus-discovery: "true" annotations: prometheus.io/path: /metrics prometheus.io/port: "8000" prometheus.io/scrape: "true" spec: type: ClusterIP selector: model.aibrix.ai/name: qwen3-embedding-8b ports: - name: serve port: 8000 targetPort: serve protocol: TCP
Сохраняем манифесты в файлы и применяем их через kubectl apply -f filename.
Запуск подов в состоянии ContainerCreating может занять 5–10 минут, так как в этот момент загружаются веса моделей с Hugging Face. Чтобы ускорить последующие запуски, вы можете подключить к кластеру NFS-хранилище, предварительно сохранить туда веса моделей и использовать их для новых подов. Пример такой настройки описан в репозитории Selectel на GitHub.
В репозитории проекта AIBrix также доступны примеры деплоя мультимодальных моделей для работы с аудио-контентом и изображениями.
Когда поды перейдут в состояние Running, проверим работоспособность моделей. Сначала узнаем IP-адрес балансировщика нагрузки AIBrix:
kubectl get svc -A | grep Load envoy-gateway-system envoy-aibrix-system-aibrix-eg-903790dc LoadBalancer 10.110.80.18 10.222.0.206 80:31131/TCP 20h
Второй адрес — External IP. Его и будем использовать.
Теперь нужно выполнить запрос из сети кластера, так как для AIBrix использовался приватный IP-адрес. Сделаем port-forward:
kubectl port-forward -n envoy-gateway-system service/envoy-aibrix-system-aibrix-eg-903790dc 9000:80
Установим переменные окружения:
export IP=127.0.0.1:9000 export TOKEN=$(kubectl get secrets vllm-api-key -ojsonpath='{.data.api-key}'|base64 -d)
И выполним запрос:
curl -v http://$IP/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -H "routing-strategy: random" \ -H "model: qwen3-8b" \ -d '{ "model": "qwen3-8b", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "help me write a random generator in python"} ], "temperature": 0.7 }'
Через некоторое время модель вернет сгенерированный ответ. Посмотреть список доступных моделей в AIBrix можно с помощью запроса:
curl http://$IP/v1/models
Cert-manager и External-dns
Эти сервисы обеспечивают интеграцию с DNS-хостингом Selectel, где делегирован ваш домен. Компонент cert-manager отвечает за выпуск SSL-сертификатов для защищенного соединения по HTTPS, а external-dns автоматически устанавливает A-записи. Они необходимы для публикации веб-интерфейса n8n.
В качестве примера в этой инструкции я использую домен outworld66.ru, вам потребуется заменить его на собственный. Это имя используется во многих конфигурационных файлах, поэтому рекомендую внимательно проверить все манифесты перед их применением.
Настройка cert-manager
Устанавливаем cert-manager в кластер:
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager --namespace cert-manager --create-namespace \ --set config.apiVersion="controller.config.cert-manager.io/v1alpha1" \ --set config.kind="ControllerConfiguration" \ --set config.enableGatewayAPI=true \ --set crds.enabled=true
Создаем секрет с учетными данными сервисного пользователя из панели управления Selectel:
kubectl create secret generic selectel-dns-credentials \ --from-literal=username= \ --from-literal=password='' \ --from-literal=account_id= \ --from-literal=project_id= \ --namespace=cert-manager
Устанавливаем cert-manager-webhook-selectel, чтобы автоматически выдавать сертификат для домена:
helm repo add selectel https://selectel.github.io/cert-manager-webhook-selectel helm repo update helm install cert-manager-webhook-selectel selectel/cert-manager-webhook-selectel -n cert-manager
Создаем ClusterIssuer:
kubectl apply -f - <<EOF apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: certmaster@selectel.ru privateKeySecretRef: name: letsencrypt-prod-account-key solvers: - dns01: webhook: groupName: acme.selectel.ru solverName: selectel config: dnsSecretRef: name: selectel-dns-credentials # Optional config, shown with default values # all times in seconds ttl: 120 # Default: 60 timeout: 60 # Default 40 EOF
Теперь можно выпустить сертификат:
kubectl apply -f - <<EOF apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: outworld-le-prod-crt namespace: envoy-gateway-system spec: dnsNames: - "*.outworld66.ru" issuerRef: group: cert-manager.io kind: ClusterIssuer name: letsencrypt-prod secretName: wildcard-outworld66-ru-tls EOF
Настройка external-dns
Создаем namespace:
kubectl create namespace external-dns
Далее создадим секрет с паролем от сервисного пользователя из панели управления. Вы можете использовать того же пользователя, что и в cert-manager.
kubectl create secret generic external-dns-selectel-webhook \ --from-literal=username= \ --from-literal=account_id= \ --from-literal=project_id= \ --from-literal=password='' \ --namespace=external-dns
Применяем манифесты external-dns и external-dns-selectel-webhook:
kubectl apply -f - <<EOF apiVersion: v1 kind: ServiceAccount metadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns rules: - apiGroups: [""] resources: ["nodes"] verbs: ["get","list","watch"] - apiGroups: [""] resources: ["pods"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: ["extensions","networking.k8s.io"] resources: ["ingresses"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["namespaces"] verbs: ["get","watch","list"] - apiGroups: ["gateway.networking.k8s.io"] resources: ["gateways","httproutes","tlsroutes","tcproutes","udproutes"] verbs: ["get","watch","list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: v1 kind: Service metadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns spec: type: ClusterIP selector: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns ports: - name: http port: 7979 targetPort: http protocol: TCP --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns strategy: type: Recreate template: metadata: labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns spec: serviceAccountName: external-dns securityContext: fsGroup: 65534 containers: - name: external-dns securityContext: capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 image: registry.k8s.io/external-dns/external-dns:v0.14.2 imagePullPolicy: IfNotPresent args: - --log-level=info - --log-format=text - --interval=1m - --source=service - --source=ingress - --source=gateway-httproute - --source=gateway-tlsroute - --source=gateway-tcproute - --source=gateway-udproute - --policy=sync # set it upsert-only if you don't want it to delete records - --provider=webhook ports: - name: http protocol: TCP containerPort: 7979 livenessProbe: failureThreshold: 2 httpGet: path: /healthz port: http initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 readinessProbe: failureThreshold: 6 httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 - name: webhook securityContext: capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 image: ghcr.io/selectel/external-dns-selectel-webhook:v0.1.0 imagePullPolicy: IfNotPresent ports: - name: http protocol: TCP containerPort: 8888 livenessProbe: failureThreshold: 2 httpGet: path: /healthz port: http initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 readinessProbe: failureThreshold: 6 httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 env: - name: PROJECT_ID valueFrom: secretKeyRef: name: external-dns-selectel-webhook key: project_id - name: ACCOUNT_ID valueFrom: secretKeyRef: name: external-dns-selectel-webhook key: account_id - name: USERNAME valueFrom: secretKeyRef: name: external-dns-selectel-webhook key: username - name: PASSWORD valueFrom: secretKeyRef: name: external-dns-selectel-webhook key: password EOF
Оригинальный манифест можно найти в репозитории на GitHub.
Балансировщик для внешних сервисов
Далее идет настройка Envoy Gateway как замена Ingress.
Укажем, что Envoy нужно запускать как daemonset, и настроим externalTrafficPolicy: Cluster.
cat <<EOF | kubectl apply -f - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: custom-proxy-config namespace: default spec: provider: type: Kubernetes kubernetes: envoyDaemonSet: {} envoyService: externalTrafficPolicy: Cluster --- apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: envoy spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy name: custom-proxy-config namespace: default EOF
Теперь можно создать балансировщик. Укажем домен, сертификат и какой порт прослушивать:
kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: eg namespace: envoy-gateway-system spec: gatewayClassName: envoy listeners: - name: https hostname: "*.outworld66.ru" port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - name: wildcard-outworld66-ru-tls namespace: envoy-gateway-system kind: Secret allowedRoutes: namespaces: from: All EOF
Установка storageclass по умолчанию
Для того, чтобы pvc заказывались без указания конкретного StorageClass, укажем один из них по умолчанию. Посмотрим, какие есть:
kubectl get storageclass
В ru-6 у меня такой:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE fast2.ru-6a cinder.csi.openstack.org Delete Immediate true 23h
Назначим класс fast2.ru-6a дефолтным:
kubectl patch storageclass fast2.ru-6a -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Qdrant
Установим векторную базу данных qdrant, в ней будем хранить векторы RAG-системы.
helm repo add qdrant https://qdrant.github.io/qdrant-helm helm repo update helm upgrade -i qdrant qdrant/qdrant -n qdrant --create-namespace

Облачные базы данных
Создайте готовую базу данных в облаке за 5 минут. Поддерживаем PostgreSQL, MySQL, Redis и не только.
postgresql operator
Установим postgres operator.
helm repo add cnpg https://cloudnative-pg.github.io/charts helm upgrade --install cnpg \ --namespace cnpg-system \ --create-namespace \ cnpg/cloudnative-pg
Подождем, пока поды оператора будут готовы:
kubectl wait pod --timeout=15m --for=condition=Ready -n cnpg-system -l app.kubernetes.io/name=cloudnative-pg
n8n
Создадим неймспейс и секрет для базы данных n8n. Пароль сгенерируем через pwgen:
kubectl create namespace n8n kubectl create secret generic db-app -n n8n \ --from-literal=username=n8n \ --from-literal="password=$(pwgen -sn1 20)"
Создадим файл с values для Helm-чарта n8n. Файл назовем n8n_values.yaml.
# https://github.com/8gears/n8n-helm-chart/blob/main/examples/values\_small\_prod.yaml #Prod like set up with CloudNativePG and nginx-ingress image: repository: docker-registry.selectel.ru/n8nio/n8n tag: 2.33.3 imagePullSecrets: - name: 8gears-registry-n8n main: service: port: 5678 config: n8n: port: 5678 webhook_url: https://n8n.outworld66.ru executions_mode: queue queue: health: check: active: true bull: redis: host: n8n-valkey port: 6379 db: type: postgresdb postgresdb: host: db-rw user: n8n # password: password is read from cnpg db-app secretKeyRef pool: size: 10 ssl: enabled: true reject_Unauthorized: true ca_file: "/home/ssl/certs/postgresql/ca.crt" #secret: # n8n: # encryption_key: "<your-secure-encryption-key>" extraEnv: &extraEnv DB_POSTGRESDB_PASSWORD: valueFrom: secretKeyRef: name: db-app key: password # Mount the CNPG CA Cert into N8N container extraVolumeMounts: &extraVolumeMounts - name: db-ca-cert mountPath: /home/ssl/certs/postgresql readOnly: true extraVolumes: &extraVolumes - name: db-ca-cert secret: secretName: db-ca items: - key: ca.crt path: ca.crt resources: limits: memory: 2048Mi requests: memory: 512Mi worker: enabled: true extraEnv: *extraEnv # using YAML magic (anchors) to reference main extraEnv extraVolumeMounts: *extraVolumeMounts # using YAML magic (anchors) to reference main extraVolumeMounts extraVolumes: *extraVolumes # using YAML magic (anchors) to reference main extraVolumes webhook: enabled: true extraEnv: *extraEnv # using YAML magic (anchors) to reference main extraEnv extraVolumeMounts: *extraVolumeMounts # using YAML magic (anchors) to reference main extraVolumeMounts extraVolumes: *extraVolumes # using YAML magic (anchors) to reference main extraVolumes valkey: enabled: true ingress: enabled: false # cnpg DB cluster request # and httproutes extraManifests: - apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: db spec: instances: 1 bootstrap: initdb: database: n8n owner: n8n postgresql: parameters: shared_buffers: "64MB" resources: requests: memory: "512Mi" limits: memory: "512Mi" storage: size: 10Gi
Обратите внимание: обязательно сгенерируйте ключ шифрования encryption_key и сохраните его в надежном месте. Без этого ключа вы потеряете доступ к данным в n8n при перезапуске сервиса:
encryption_key=$(pwgen -sn1 50)
Установим n8n с параметрами (values) из этого репозитория. И установим encryption_key через флаг --set:
helm upgrade --install n8n oci://8gears.container-registry.com/library/n8n --version 2.0.1 \ --create-namespace -n n8n -f n8n_values.yaml \ --set main.secret.n8n.encryption_key="$encryption_key"
Запуск всех компонентов n8n может занять некоторое время. Отслеживать статус запуска подов можно c помощью команды:
kubectl get pods -n n8n
Дождемся, пока все поды перейдут в состояние Running. Получим доступ к веб-панели и безопасно настроим административный доступ:
kubectl port-forward -n n8n service/n8n 3001:5678
На данном этапе сервис доступен локально: http://127.0.0.1:3001.
Опубликуем сервис с помощью Envoy Gateway с использованием облачного балансировщика нагрузки:
kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: n8n namespace: n8n spec: hostnames: - n8n.outworld66.ru parentRefs: - name: eg namespace: envoy-gateway-system rules: - backendRefs: - name: n8n kind: Service port: 5678 matches: - path: type: PathPrefix value: / - backendRefs: - name: n8n-webhook kind: Service port: 80 matches: - path: type: PathPrefix value: /webhook/ - path: type: PathPrefix value: /webhook-test/ - path: type: PathPrefix value: /webhook-waiting/ - path: type: PathPrefix value: /form/ EOF
Настройка RAG-пайплайна
Наконец можно перейти в панель (в моем случае это https://n8n.outworld66.ru) и начать настройку. Будем использовать готовый RAG-сценарий. Оригинал доступен по ссылке.
Создаем Workflow:

Импортируем существующий пайплайн по URL:

Для импорта укажем следующую ссылку: https://raw.githubusercontent.com/selectel/webinar-llm-on-mks/refs/heads/main/apps/04.llm-clients/02.n8n/Demo_RAG_n8n_webinar.json

Пайплайн выглядит следующим образом:

Обратите внимание на узлы, отмеченные красными предупреждающими значками. В них необходимо указать данные для авторизации и выполнить дополнительную настройку. Рассмотрим каждый из них подробнее.
Элемент Webhook понадобится только в том случае, если вам необходимо интегрировать сценарий с каким-либо внешним сервисом.
Qdrant
Укажем учетные данные для Qdrant:

Поскольку авторизация в Qdrant не настроена, зададим только URL-адрес: http://qdrant.qdrant.svc.cluster.local:6333

После сохранения параметров соединение будет успешно установлено:

Если при обработке и загрузке больших документов возникнут проблемы, рекомендуется уменьшить размер пакета векторизации (Embedding Batch Size) до 100.
OpenAI Chat Model

В поле адреса сервера укажем IP-адрес шлюза AIBrix. Его можно узнать с помощью команды:
kubectl get svc -A | grep Load envoy-gateway-system envoy-aibrix-system-aibrix-eg-903790dc LoadBalancer 10.110.80.18 10.222.0.206 80:31131/TCP 22h envoy-gateway-system envoy-envoy-gateway-system-eg-5391c79d LoadBalancer 10.106.131.36 135.106.154.81 443:30933/TCP 30m
В моем случае это адрес 10.222.0.206.
Также получим токен для доступа к LLM из секретов Kubernetes:
kubectl get secrets vllm-api-key -ojsonpath='{.data.api-key}'|base64 -d
Внесем эти параметры и сохраним изменения:

Соединение успешно установлено:

В поле выбора модели укажем целевую языковую модель — qwen3-8b.

Embeddings OpenAI
Модели используют тот же токен авторизации, поэтому мы выбираем ранее созданные учетные данные. В качестве используемой модели укажем qwen3-embedding-8b:

Redis Chat Memory
Задаем учетные данные для блока Redis Chat Memory.

В поле Host указываем адрес n8n-valkey:

После сохранения параметров подключение будет успешно установлено:


Как вы внедряете ИИ?
Поделитесь своим опытом за 7–10 минут. Разыгрываем 5 сертификатов по 1 500 ₽ на маркетплейсе и 10 наборов мерча.
Тестовый запрос
Отправим агенту тестовое сообщение (пинг) через чат n8n — он должен прислать ответ:

Деактивируем блок Webhook и опубликуем пайплайн:

Попробуем запустить пайплайн через оранжевую кнопку Execute workflow:

В открывшейся форме загружаем тестовый файл — я использую книгу в формате PDF:

Пайплайн обработает файл и успешно завершит свою работу:

После завершения обработки зададим вопрос по содержанию книги, указав агенту на необходимость поиска в векторном хранилище. В пайплайне видно, как именно агент обращается к векторной базе данных:

Если вы планируете использовать n8n в промышленной эксплуатации (Production), рекомендуется своевременно обновлять платформу и отслеживать изменения в ее официальном Helm-чарте.