Это минимальный на мой взгляд набор приложений, чтобы показать путь от голого кластера до полностью автоматизированной платформы и хочу рассказать о нём так, как это интересно DevOps-инженеру.
Зачем DevOps-инженеру MLOps?
По данным отраслевых исследований, до 80% ML-проектов так и остаются в статусе «эксперимент». Модели обучают в Jupyter-ноутбуках, но до продакшена они доходят редко — потому что нет внятных процессов, как их воспроизводить, тестировать и выкатывать.
Корень проблемы — разрыв между двумя культурами. Дата-сайентист мыслит экспериментом: подобрал параметры, погонял тесты, получил приемлимые на его взгляд результаты — работа сделана. DevOps-инженер мыслит сервисом: код в Git, тесты в CI, сборка, деплой, мониторинг, алерты.
Теперь давайте сюда добавим стейк-холдеров из бизнес-составляющей, что мы получим?
Приходит владелец продукта к devops-инженеру и говорит: вот у нас появился ML-инженер - сделай так, чтобы он мог работать!
Да простят меня коллеги, но Дата-сайентисты - люди творческие (хоть и математики в основном) - им сложно даже иногда объяснить, что такое git, как все положить в docker и причем здесь devsecops (это все из личного опыта, не более - на всех не распространяется!)?
Из личного примера знаю, что если дать им просто сервер с GPU и поставить туда Jupyter - в итоговой сборке приложения с моделью внутри окажется и весь код git репозитория, включая ключи и пароли. И Дата-сайентист тоже не может объяснить devops инженеру, что ему надо.
Как оценить проделанную работу Дата-сайентиста? Как понять, что он чем-то занят? - нужно каким-то образом фиксировать его работу - наборы данных, обученные модели и т.д.
Когда эти двое пытаются запустить ML-сервис без общего процесса — получается боль.
MLOps — это как раз тот клей, который соединяет два мира. На практике это означает, что все те практики, которые DevOps-инженер считает базовыми — версионирование, CI/CD, контейнеризация, мониторинг, секреты — распространяются ещё и на данные и модели. А это чуть сложнее, чем кажется на первый взгляд.
Чем ML-пайплайн отличается от обычного CI/CD
В классической разработке артефакт — это код, который ведёт себя детерминированно. В ML поведение модели зависит от трёх вещей: кода, данных и гиперпараметров. Поэтому:
Данные — артефакт первого класса. Недостаточно закоммитить код в Git. Нужно версионировать датасеты, причём так, чтобы по хешу коммита можно было воспроизвести любой эксперимент. Git для этого не подходит — датасеты весят гигабайты. Мы использовали DVC (Data Version Control), который хранит в Git только лёгкие .dvc-указатели, а сами файлы — в S3-совместимом хранилище MinIO.
Эксперименты вместо детерминированных сборок. В CI/CD сборка либо зелёная, либо красная. В ML нужно сравнивать десятки запусков с разными алгоритмами и гиперпараметрами, чтобы выбрать лучший. Для этого нужен трекинг экспериментов — мы взяли MLflow с PostgreSQL в качестве бэкенда.
Дрейф модели. Самая неприятная особенность ML в продакшене: модель может деградировать без единого изменения в коде. Просто потому, что распределение входных данных со временем меняется (concept drift). Поэтому жизненный цикл ML-модели — циклический, а не линейный. Модель нужно мониторить и переобучать.
Model Registry — аналог Container Registry. Чтобы выкатить модель в prod, её нужно где-то хранить, версионировать и уметь быстро откатить. MLflow Model Registry с алиасами (@champion, @challenger) решает эту задачу. Сервис приложения всегда грузит модель по алиасу @champion — чтобы сменить версию, достаточно переназначить алиас, не меняя код (тут конечно зависит от вашего рабочего флоу).
Если перевести на язык DevOps: CI/CD в ML — это пайплайн, в котором артефактами являются не только Docker-образы, но и обученные модели, а стадия деплоя обновляет не только код, но и версию модели через Model Registry.
Архитектура: что и зачем мы развернули
Платформа спроектирована для работы в закрытом контуре: приватный GitLab, приватный Docker Registry, внутренний Ingress, никаких облачных сервисов. Все компоненты — open-source. Ну и напомню, что речь идет о минимальном наборе (прототип!) - при увеличении "объемов" архитектуру, конечно, стоит пересматривать.
Два контура
Кластер разделён на два физических контура:
Kubernetes-кластер (serving & platform). Здесь крутится всё, что не требует GPU: хранение данных, трекинг экспериментов, секреты, мониторинг, а главное — сервис инференса, который отвечает на запросы пользователей.
GPU-сервер (training node, вне K8s). Отдельная машина с GPU NVIDIA, не входит в кластер. Используется только для обучения моделей — через Docker-контейнеры с nvidia-container-toolkit. Почему вне кластера? GPU-железо дорогое, и привязывать его к K8s-ноде означает, что оно не может шариться между проектами. А так — один GPU-сервер обслуживает обучение, пока кластер занят своим делом.
Технологический стек
Компонент | Технология | Зачем |
|---|---|---|
Оркестрация | Kubernetes | Базовая платформа |
GitOps / CD | ArgoCD | Декларативный деплой из Git |
CI | GitLab CI/CD | Тесты, сборка, сканирование |
Хранилище | MinIO (S3) | Данные и артефакты MLflow |
Трекинг ML | MLflow + PostgreSQL | Эксперименты + Model Registry |
ML-приложение | FastAPI | REST API предсказаний |
Версионирование данных | DVC | Версии датасетов |
Среда экспериментов | JupyterLab | Интерактивная разработка |
Обучение | Celery + Redis | Очередь GPU-задач |
Секреты | OpenBao(Vault) + ESO | Централизованное хранение |
SAST | Bandit | Анализ Python-кода |
Сканирование образов | Trivy | CVE в контейнерах |
Мониторинг | Prometheus + Grafana | Метрики API |
Связь между контурами — через сеть: GPU-сервер ходит в MLflow, MinIO и Redis через Ingress/LoadBalancer. С точки зрения безопасности это плюс: компрометация GPU-сервера не даёт доступа к управляющим компонентам k8s.
Теперь — почему мы не взяли популярные инструменты.
Kubeflow. Стандарт де-факто для MLOps на K8s, но он проектировался под облака. Развернуть Kubeflow в закрытом контуре — тоже можно, но для старта может быть сложно из-за непонимания необходимого набора модульных микросервисов. Плюс он тащит за собой CRD, которые усложняют отладку. Для прототипа — избыточно.
Airflow. Отличный оркестратор, но его модель «DAG по расписанию» плохо ложится на событийную природу ML-экспериментов. Дата-сайентист хочет запустить обучение «прямо сейчас» из ноутбука, а не ждать ближайшего scheduled run. Мы использовали Celery + Redis как более легковесную альтернативу.
HashiCorp Vault. Проблема в лицензии: после перехода HashiCorp на BSL (Business Source License) использование Vault в production стало рискованным. Мы взяли OpenBao — открытый форк под MPL 2.0, полностью совместимый по API.
GitOps: Apps-of-Apps
GitOps-репозиторий организован по принципу Apps-of-Apps. Корневой ArgoCD Application управляет набором дочерних Applications — по одному на компонент. Порядок развёртывания задаётся sync-wave - позволяет избежать ситуаций, когда приложение хочет запуститься, а секреты из волта еще не подтянулись.
Структура репозитория:
gitops/ ├── apps/ # ArgoCD Applications (по одному на компонент) ├── charts/ # Завендоренные Helm-чарты (mlflow, postgresql, redis, ...) ├── helm-values/ # Helm values каждого компонента ├── manifests/ # Нативные K8s-манифесты (inference, monitoring) ├── secrets/ # ExternalSecret-манифесты (OpenBao → K8s) ├── namespaces/ # Namespace с TLS-лейблами ├── bootstrap/ # root-app.yaml (применяется один раз вручную) └── docker/sops-cmp/ # Dockerfile для ArgoCD CMP sidecar (SOPS)
Каждый ArgoCD Application выглядит однотипно. Например, MLflow:
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: mlflow namespace: argocd annotations: argocd.argoproj.io/sync-wave: "3" spec: project: default sources: - repoURL: https://code.appsec-team.ru/mlops/mlops-platform.git path: charts/mlflow targetRevision: main helm: valueFiles: - $values/helm-values/mlflow-values.yaml - repoURL: https://code.appsec-team.ru/mlops/mlops-platform.git targetRevision: main ref: values destination: server: https://kubernetes.default.svc namespace: mlflow syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true
Чарт скопирован прямо в Git-репозиторий (gitops/charts/mlflow/) — никаких внешних Helm-репозиториев. В закрытом контуре это единственно надёжный вариант: не нужно зеркалировать чарты, нет риска, что внешний репозиторий станет недоступен, и все изменения чарта проходят code review наравне с values. Два источника (sources) в одном Application — фича ArgoCD 2.6+ — ссылаются на один и тот же Git-репозиторий, просто на разные пути в нём.
Закрытый контур: главные боли и решения
Самая сложная часть работы в изолированной среде — не архитектура, а логистика артефактов. Всё, что в облаке доступно по щелчку, здесь нужно сначала затащить внутрь периметра.
Приватный Docker Registry
Все образы проходят через приватный docker registry, например, Harbor. Helm-чарты часто ссылаются на публичные образы (docker.io, quay.io) — их нужно перекладывать в приватный registry и править values.
image: registry: registry.appsec-team.ru repository: public/bitnami/postgresql
SOPS вместо облачных KMS
В облаке секреты GitOps-репозитория шифруются через KMS. В закрытом контуре мы используем SOPS с ключами age. ArgoCD расшифровывает их через CMP sidecar — кастомный плагин, который вызывает sops --decrypt перед применением манифеста.
# argocd-values.yaml (фрагмент) controller: extraContainers: - name: sops-cmp image: registry.appsec-team.ru/public/argocd-sops-cmp:3.12.2 command: [/var/run/argocd/argocd-cmp-server] volumeMounts: - name: sops-age-key mountPath: /root/.config/sops/age/keys.txt subPath: keys.txt
Age key монтируется как Secret и никогда не попадает в Git. Манифесты с секретами коммитятся в зашифрованном виде (.enc.yaml).
OpenBao вместо Vault
OpenBao — форк Vault под лицензией MPL 2.0, полностью совместимый по API. Развёрнут в режиме HA с тремя репликами в K8s. Все секреты приложений хранятся в KV-хранилище с иерархией по компонентам.
External Secrets Operator (ESO) автоматически синхронизирует значения из OpenBao в K8s Secrets. Манифест ExternalSecret описывает mapping:
apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: mlflow-credentials namespace: mlflow spec: refreshInterval: 5m secretStoreRef: name: cluster-secret-store kind: ClusterSecretStore target: name: mlflow-credentials data: - secretKey: MLFLOW_TRACKING_USERNAME remoteRef: key: secret/mlflow property: trackingUsername - secretKey: MLFLOW_TRACKING_PASSWORD remoteRef: key: secret/mlflow property: trackingPassword
ExternalSecret может тащить данные из нескольких путей KV в один Kubernetes Secret. Мы использовали это, когда понадобилось передать MLFLOW_TRACKING_USERNAME и MLFLOW_TRACKING_PASSWORD в singleuser-поды JupyterLab из того же секрета, что используется для MLflow и inference.
Сеть и TLS
Все сервисы публикуются через nginx Ingress Controller (класс nginx-internal) с единым wildcard TLS-сертификатом. Балансировщик — MetalLB с внутренним IP (наружу ничего не торчит). Сертификат автоматически распространяется по всем namespace через ClusterExternalSecret:
apiVersion: external-secrets.io/v1 kind: ClusterExternalSecret metadata: name: tls-wildcard spec: externalSecretName: tls-wildcard namespaceSelectors: - matchLabels: mlops.platform/tls: "true" externalSecretSpec: # ... тянет tls.crt + tls.key из OpenBao
Достаточно добавить лейбл mlops.platform/tls: "true" на новый namespace — и сертификат появится там автоматически.
Некоторые компоненты: что важно знать
MLflow: почему не SQLite
Стандартная установка MLflow по умолчанию использует SQLite. Этого хватает ровно до момента, когда вы запускаете больше одного воркера. Gunicorn с несколькими worker-процессами + SQLite :memory: = каждый воркер со своей изолированной БД. Результат: RESOURCE_DOES_NOT_EXIST при попытке записать параметр — run создался в одном воркере, а log_param пришёл в другой.
Решение: PostgreSQL как backend store. В community-charts/mlflow 1.8.1 это делается через existingDatabaseSecret:
backendStore: existingDatabaseSecret: mlflow-db-credentials existingDatabaseSecretKey: DATABASE_URL postgres: enabled: true
MLflow защищён базовой HTTP-аутентификацией через аннотации Nginx Ingress — не через встроенную аутентификацию MLflow (которая платная), а на уровне Ingress:
ingress: enabled: true annotations: nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/auth-secret: mlflow-basic-auth
GPU-сервер: мета-воркер
Обучение на GPU организовано через очередь задач. Идея в том, что GPU-сервер не знает ничего о конкретных ML-проектах — он просто получает задачу вида «запусти Docker-образ X с командой Y и переменными окружения Z».
Задача описывается четырьмя полями:
# dispatch.py — отправка задачи из JupyterLab или GitLab CI task_data = { "project": "churn-model", "image": "registry.appsec-team.ru/mlops/churn-worker:latest", "command": "python src/train.py --algorithm xgboost --n_estimators 200", "env": { "MLFLOW_TRACKING_URI": "https://mlflow.service.appsec-team.ru", "AWS_ACCESS_KEY_ID": "...", }, } result = celery_app.send_task("train_model", args=[task_data], queue="train")
Мета-воркер на GPU-сервере принимает задачу из Redis, делает docker pull образа и запускает контейнер:
# meta-worker — Celery task handler @app.task(name="train_model", bind=True) def train_model(self, task_data): project = task_data["project"] image = task_data["image"] command = task_data["command"] env = task_data.get("env", {}) client = docker.from_env() container = client.containers.run( image=image, command=command, environment=env, runtime="nvidia", device_requests=[docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])], remove=True, detach=False, ) return {"status": "completed", "exit_code": container.wait()["StatusCode"]}
Docker Socket Proxy — почему это важно
Мета-воркер подключается к Docker-демону GPU-сервера. Если дать ему прямой доступ к /var/run/docker.sock, это эквивалентно root-доступу к хосту: процесс внутри контейнера может запускать произвольные контейнеры, монтировать файловую систему хоста и так далее.
Решение: прокси linuxserver/socket-proxy, который встаёт между воркером и сокетом Docker и разрешает только минимально необходимые операции:
# docker-compose.yml на GPU-сервере services: socket-proxy: image: lscr.io/linuxserver/socket-proxy environment: - IMAGES=1 # docker pull - CONTAINERS=1 # docker run, docker logs - POST=1 # docker wait # всё остальное — 0 (заблокировано) volumes: - /var/run/docker.sock:/var/run/docker.sock:ro worker: image: registry.appsec-team.ru/mlops/meta-worker:latest environment: - DOCKER_HOST=tcp://socket-proxy:2375 # не /var/run/docker.sock!
Это реализует принцип минимальных привилегий: даже при компрометации воркера атакующий не получит доступ к управлению всей инфраструктурой хоста, а только к тем операциям Docker API, которые разрешены прокси.
MinIO: два Ingress'а — не баг, а фича
MinIO поднимает два HTTP-сервера на разных портах: S3 API на 9000 и веб-консоль на 9001. Соответственно, нужно два Ingress:
minio.service.appsec-team.ru→ порт 9000 (S3 API для DVC, MLflow, boto3)s3.service.appsec-team.ru→ порт 9001 (веб-консоль для администратора)
Почему нельзя обойтись одним? Если открыть S3 API в браузере, MinIO автоматически редиректит на порт 9001 (консоль). Но Ingress не проксирует этот порт на том же домене — страница будет пустой. Разные домены для API и консоли — осознанное решение.
JupyterLab: OAuth и проброс секретов
JupyterHub развёрнут через Helm-чарт jupyterhub/jupyterhub (Zero to JupyterHub). Аутентификация — через Keycloak SSO с GenericOAuthenticator:
hub: config: JupyterHub: authenticator_class: generic-oauth GenericOAuthenticator: client_id: jupyterlab client_secret: "..." # из OpenBao через existingSecret authorize_url: https://keycloak.service.appsec-team.ru/realms/mlops/protocol/openid-connect/auth token_url: https://keycloak.service.appsec-team.ru/realms/mlops/protocol/openid-connect/token userdata_url: https://keycloak.service.appsec-team.ru/realms/mlops/protocol/openid-connect/userinfo oauth_callback_url: https://jupyter.service.appsec-team.ru/hub/oauth_callback
Env-переменные для singleuser-подов (MLflow credentials, MinIO keys) пробрасываются через ExternalSecret. Важно: extraEnv в z2jh-чарте принимает dict-формат, а не K8s-список:
singleuser: extraEnv: MLFLOW_TRACKING_URI: "https://mlflow.service.appsec-team.ru" MLFLOW_TRACKING_USERNAME: valueFrom: secretKeyRef: name: jupyterlab-credentials key: MLFLOW_TRACKING_USERNAME AWS_ACCESS_KEY_ID: valueFrom: secretKeyRef: name: jupyterlab-credentials key: AWS_ACCESS_KEY_ID
CI/CD: от git push до production
Конвейер описан в .gitlab-ci.yml и запускается при каждом push в main.
Сборка образов с кешированием
Сборка идёт через docker buildx на DinD-раннере. Кеш слоёв пишется в реестр образов — это критично для образа с PyTorch, который без кеша собирается 15 минут:
build:api: stage: build image: docker:24-dind script: - docker buildx build --cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache:api --cache-to type=registry,ref=$CI_REGISTRY_IMAGE/cache:api,mode=max -f Dockerfile.api -t $CI_REGISTRY_IMAGE/churn-api:$CI_COMMIT_SHORT_SHA --push .
Ключевой момент — порядок инструкций в Dockerfile. Сначала копируются файлы зависимостей и ставится pip install, потом копируется изменяемый код:
# Сначала зависимости (меняются редко → слой кешируется) COPY requirements.api.txt . RUN pip install --no-cache-dir -r requirements.api.txt # Потом код (меняется часто → инвалидирует только последний слой) COPY src/ ./src/
При таком порядке правка .py-файла не инвалидирует слои с установленными пакетами — и время сборки падает с 3 минут до 20–40 секунд.
Стадии безопасности
Bandit проверяет Python-код на типичные уязвимости (опасные функции, потенциальные инъекции, слабая криптография) на уровне medium и выше:
sast:bandit: stage: sast script: - pip install bandit - bandit -r src/ -ll -f txt -o bandit-report.txt artifacts: paths: - bandit-report.txt
Trivy сканирует собранный образ на CVE в пакетах ОС и Python-зависимостях:
scan:trivy: stage: scan image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL --ignore-unfixed $CI_REGISTRY_IMAGE/churn-api:$CI_COMMIT_SHORT_SHA - trivy image --severity HIGH,CRITICAL --format template --template "@contrib/gitlab.tpl" -o trivy-report.json $CI_REGISTRY_IMAGE/churn-api:$CI_COMMIT_SHORT_SHA artifacts: paths: - trivy-report.json
DevSecOps: безопасность ML-конвейера
В ML-системах безопасность часто отходит на второй план — «лишь бы модель работала». Мы попытались исправить этот перекос.
Два уровня секретов
Первый уровень — SOPS + age для GitOps-credentials. ArgoCD-секреты (токены, ключи) шифруются перед коммитом и расшифровываются CMP-сайдкаром ArgoCD в рантайме. Они никогда не появляются в открытом виде в Git.
Второй уровень — OpenBao + External Secrets Operator для всех остальных секретов (пароли БД, ключи MinIO, токены сервисов). Манифесты ExternalSecret лежат в Git открыто (они лишь описывают, откуда взять значение), а сами значения живут в OpenBao.
Безопасность контейнера
Образ API собирается от непривилегированного пользователя:
RUN useradd --create-home --uid 1000 appuser USER appuser
В Kubernetes-деплойменте задан строгий SecurityContext:
securityContext: runAsNonRoot: true runAsUser: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault
Для временных файлов uvicorn монтируется emptyDir volume на /tmp.
Что осталось за скобками
Auto-Unseal OpenBao. В прототипе при перезапуске OpenBao требуется ручной unseal (три ключа из пяти). Для production нужно настроить Auto-Unseal через Transit Seal или облачный KMS — но в закрытом контуре облачного KMS нет, поэтому остаётся разворачивать отдельный Transit-кластер или мириться с ручным unseal.
Защита модели. Модель хранится в MinIO как pickle-файл. Pickle может содержать произвольный код — это известный вектор атаки. В прототипе мы полагаемся на то, что модель сохраняется доверенным кодом в доверенном окружении, но для production стоит рассмотреть safer-форматы (ONNX, MLflow pyfunc с проверкой подписи).
Мониторинг: не только latency
FastAPI-сервис экспортирует метрики через prometheus-fastapi-instrumentator на эндпоинте /metrics. Библиотека автоматически собирает:
http_requests_total— счётчик запросов с разбивкой по handler, method, status_codehttp_request_duration_seconds— гистограмма задержки (p50, p95, p99)http_requests_in_progress— количество одновременно обрабатываемых запросов
Prometheus забирает метрики через ServiceMonitor:
apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: inference namespace: monitoring spec: selector: matchLabels: app: inference endpoints: - port: http path: /metrics interval: 15s timeout: 10s
Grafana-дашборд «MLOps — Inference Service» содержит восемь панелей:
Stat-панели: RPS, Error Rate 5xx (пороги 1%/5%), Latency p99 (пороги 100ms/500ms), Total Requests (1h)
Timeseries-панели: Latency Percentiles (p50/p95/p99), Request Rate by Status Code (2xx/4xx/5xx), RPS по эндпоинтам, Memory Usage
Дашборд хранится в GitOps-репозитории как ConfigMap и применяется ArgoCD:
apiVersion: v1 kind: ConfigMap metadata: name: inference-dashboard labels: grafana_dashboard: "1" data: dashboard.json: | # ... JSON дашборда ...
Что НЕ вошло в прототип
Мониторинг дрейфа данных (concept drift) и ML-метрик (точность модели на живых данных) — самая важная часть MLOps-мониторинга, которая осталась за рамками прототипа. Для production нужно отслеживать распределение входных признаков и бить тревогу, когда оно начинает отличаться от тренировочного.
С чего начать: минимальный MLOps
Если вы DevOps-инженер и хотите построить MLOps-платформу в своей компании, вот минимальный набор, с которого стоит начать:
Шаг 1: GitOps-репозиторий + ArgoCD. Без этого всё остальное будет ручным бардаком. Настройте Apps-of-Apps, разбейте компоненты по sync-wave, настройте SOPS для секретов.
Шаг 2: MLflow + PostgreSQL + MinIO. Это ядро платформы: трекинг экспериментов, хранение артефактов, Model Registry. Всё остальное — либо обвес, либо интеграция.
Шаг 3: JupyterLab. Дата-сайентистам нужно где-то работать. Настройте SSO, пробросьте credentials в env, установите MLflow SDK и DVC в образ singleuser.
Шаг 4: GitLab CI с базовыми стадиями. Тесты → Bandit → сборка образа → Trivy → деплой через ArgoCD. Стадию обучения можно добавить позже.
Что можно отложить:
GPU-сервер и мета-воркер — если модели обучаются быстро на CPU, очередь задач не нужна
OpenBao — для начала можно использовать sealed secrets или age-зашифрованные секреты в Git (уже настроено на шаге 1)
Prometheus/Grafana — можно подключить позже, когда сервис начнёт получать реальный трафик
Keycloak SSO — для прототипа хватит встроенной basic-auth JupyterHub
Итоги
Что получилось в результате:
15 приложений в ArgoCD — вся платформа управляется декларативно из Git
2 точки входа в обучение — JupyterLab и GitLab CI, обе ведут в одну очередь Redis
0 секретов в коде — всё в OpenBao или зашифровано SOPS
Полный цикл от git push до production
Инференс: модель загружается один раз при старте пода, дальше работает из памяти
Платформа не идеальна — это прототип, и я сознательно оставил за скобками мониторинг дрейфа данных, Auto-Unseal OpenBao, A/B-тестирование моделей и оркестрацию параллельных обучений. Но она показывает главное: MLOps — это не магия и не отдельная вселенная со своими законами. Это просто DevOps-практики, распространённые на артефакты ML-жизненного цикла.
Если вы DevOps-инженер, вы уже знаете 80% того, что нужно для построения MLOps-платформы. Остальные 20% — это специфика ML: Model Registry, версионирование данных, очередь GPU-задач. С этим уже можно разбираться по мере внедрения.
Ну и для тех, кто долистал, ссылка на мои изыскания:
https://gitverse.ru/vsb2007/mlops-platform
Надеюсь был полезен :-)
Спасибо за внимание!

