
Выход в продакшен с собственными языковыми моделями внутри корпоративного контура часто упирается в высокую стоимость 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-чарте.
Автор: outworld66


