Привет, Хабр! Меня зовут Кирилл Кулаков, я занимаюсь развитием MLOps-платформы в Uzum Fintech.

Недавно у нас в команде разгорелся спор о том, как правильно разворачивать платформы. Причем разгорелся он уже после того, как мы совместно всё спроектировали, двигались какое-то время в одном инфополе, и я уже развернул довольно большую часть.

И тут коллеги начали задавать вопросы «Почему мы не разворачиваем каждый компонент последовательно, настраивая все досконально?». Для меня это звучало как: «Ты сделал неправильно, сейчас будем разбирать твою работу и от половины откажемся». 

Мы строим MLOps-платформу под широкий спектр AI-задач:

  • тут и задачи классического ML (кредитный скоринг, антифрод);

  • инференс — классические ML-алгоритмы (бустинги), LLM/VLM модели;

  • пайплайны на основе LLM (Мультиагентные системы /  RAG).

И по ходу работы постоянно сталкиваемся с архитектурными вызовами и альтернативными решениями. 

В какой-то момент мы поняли: для одной и той же задачи мы можем собрать множество архитектурных вызовов, а потом тестировать такое же множество разных подходов. А время всё ещё не резиновое, как и силы команды. Так что мы сели и серьёзно обсудили наши методы развёртывания ИТ-платформ.

И потом я задумался, а на самом ли деле в разных компаниях и командах бывают настолько разные подходы к этому делу?

В статье предлагаю поисследовать эту тему вместе со мной.

Борьба подходов

Перед нами два подхода:

1. Сначала развернуть все запроектированные компоненты (FluxCD, Vault, External-Secret, MLflow, Airflow, JupyterHub, Keycloak и прочее), и уже потом их донастраивать, интегрировать между собой и тестировать — «горизонтальный взрыв».

2. Сразу разворачивать каждый компонент, протестированный, и только потом переходить к следующему — «вертикальный срез».

Я выбрал первый подход, так как это тестовый кластер (будущий dev-контур), и продуктовый подход — когда мы делаем MVP какой-либо системы, который должен содержать минимально необходимый набор нужных компонентов системы для демонстрации всей идеи на конвейере на котором можно создавать модели и их использовать. В компании такого не строили, сроки начала использования всей платформы тоже пока не очень понятны.

Набор компонентов и взаимодействие между ними, равно как и подводные камни с интеграцией между сервисами, вызывали у меня очевидные опасения.

Работая 13 лет в ИТ, я уже много раз видел, как вылизанный красивый компонент системы при внедрении нового вдруг становился ненужным и отправлялся в топку, вместе с затраченным на него временем и усилиями.

Поэтому я для себя выработал принцип достаточности:

Если требования к завершенности компонента системы четко не предъявлены, то минимально достаточным Definition of Done будет запущенный и работающий компонент системы без ошибок в логах, со стандартным минимальным набором зависимостей.

Дальше тестируем весь pipeline, доводя весь конечный результат «от данных до использования модели».

Но, как мы знаем, истина всегда где-то посередине. И об этом я расскажу в конце статьи.

Рассмотрим оба подхода подробнее.

Подход А

Разворачиваем всё (FluxCD, Vault, External-Secret, Ray, Keycloak, Redis, Postgres, Feast, MLflow, …), потом отлаживаем и донастраиваем — MVP платформы.

Плюсы

  • Параллелизация работы инженеров по широкому фронту сервисов при условии использования GitOps (один раскатывает MLflow, другой в это время катит JupyterHub с обязательным сохранением в git).

  • Быстро видим сразу весь продукт (весь стек и конечный результат MLOps-платформы).

  • Доказательство, что все вообще соберется в один кластер.

  • Ждем не 6-12 месяцев, а 2-3 (с учетом того, что не со всеми компонентами работали, но helm и kubectl мы умеем пользоваться).

Минусы

  • Нет короткой обратной связи «деплой → проверка → откат»: нарушается дух маленьких партий изменений, с которым коррелирует производительность поставки в исследованиях DORA (см. также книгу Accelerate — Nicole Forsgren, Jez Humble, Gene Kim).

  • Чем больше компонентов поднимается одновременно, тем сложнее диагностика. Количество потенциальных точек отказа и связей между сервисами быстро растёт, поэтому поиск причин проблем занимает больше времени.

  • Выше вероятность комбинаторных отказов по зависимостям в сервисах (External Secrets еще не отдает секрет для Postgres, Postgres лежит, и Airflow не может подняться).

  • Операционный долг накапливается до первой рабочей ценности для потребителя платформы (ML-инженер всё ещё не может залогиниться и зарегистрировать модель).

  • Риск неправильного порядка и нарушение идемпотентности (повторяемости с одинаковым ожидаемым результатом) секретов/БД/миграций без единого источника правды, что ломает повторяемость окружений (антипаттерн для 12-Factor App-подхода и GitOps-дисциплину).

В мировой практике это считается нормой в ограниченном виде: короткий proof-of-concept / hackathon cluster / dev / staging, который можно выкинуть/пересобрать лабораторию под конкретный RFP (стенд, который собирают под требования конкретного тендера/запроса клиента). Это не эталон для production-платформы без жестких волн и критериев готовности.

Подход Б

Каждый компонент последовательно: полная настройка + тесты, затем следующий компонент.

Плюсы

  • Малый радиус изменений: проще отслеживать и онбордить (сейчас на платформе только Vault).

  • Явный Definition of Done на сервис: healthchecks, backup/restore где нужно, runbook, SLO-заготовка:такой подход хорошо сочетается с практиками SRE и эксплуатационной зрелостью Google SRE Book.

  • Естественный порядок зависимостей: сначала базовая инфраструктура (сеть, storage, ingress, базы данных), потом приложения, которые от них зависят.

Минусы

  • Интеграционные сюрпризы всплывают в конце: по отдельности сервисы работают, но в связке возникают конфликты , (MTLS политики, OIDC редиректы).

  • Риск перепроектирования: можно идеально настроить MLflow, а потом выяснить, что Feast требует другой модели доступа к фичам).

  • Время до сквозного сценария (deploy model → inference → observability) может сдвинуться на квартал, если нет явных промежуточных интеграционных вех.

На практике зрелые платформенные команды редко используют крайние подходы в чистом виде. Обычно применяют поэтапное развитие платформы:

  • с критериями готовностями;

  • интеграционными вехами;

  • и регулярной проверкой сквозных сценариев.

На это же указывают исследования DORA и CNCF Platform Engineering Maturity Model: маленькие изменения и частая интеграция почти всегда устойчивее больших взрывных релизов.

На практике же лучше всего работает гибридный подход: что-то похожее на выбор методов слияния моделей.

Мы пришли к модели волн: когда платформа развивается слоями, а каждый слой даёт системе новую полезность.

№ волны

Что появляется

Что это нам даёт

0

Kubernetes, GitOps, ingress, observability

Базовый фундамент

1

Keycloak, Vault, RBAC

Auth и secrets

2

PostgreSQL, Redis, S3

Data layer

3

MLflow, Feast, Airflow

ML pipeline

А теперь подробнее про каждую волну.

Wave 0 — фундамент платформы

Тут у нас железо, terraform, GitOps, ingress, tls, observability.

  • Собрать требования, спроектировать систему

  • Развернуть/получить серверные мощности, доступы

  • Подготовить виртуальные машины, сети, хранилища (IaC Terraform + Ansible)

  • Поднять Kubernetes-кластер, CNI, CSI

  • Развернуть ingress-контроллер + certs

  • Развернуть GitOps control plane (fluxcd + git + reconcile policy).

  • Развернуть базовое observability (fluentbit, loki, grafana, Victoria/Prometheus)

OpenTelemetry/Jaeger — в целом зависит от задач, но тоже можно сразу добавить.

Что считаем результатом:

  • Фундамент платформы создан

  • Flux исключает ручное управление типа kubectl apply.

  • Ingress, обмазанный TLS, проксирует запросы к UI какого-то helloworld сервиса.

  • Сертификаты автоматически выпускаются

  • Видны базовые метрики работы кластера и компонентов

  • Есть runbook, базовая документация «как откатить неудачный релиз через GitOps»

Wave 1 — идентичность

На этом уровне — Keycloak и Vault

  • Развернут Keycloak (или ваш IdP:identity provider)

  • Vault/External Secrets (или эквивалент секретного слоя)

  • Service accounts / RBAC / минимальная модель прав между сервисами

  • Базовые OIDC/OAuth-интеграции для платформенных сервисов (пока хотя бы 1-2 эталонных)

Эти сервисы относят ко второму эшелону, потому что ML-сервисы и data plane требуют стабильной auth+secrets-models. Если это отложить, позже обычно начинается массовый рефакторинг конфигураций и доступов.

Что считаем результатом:

  • Работает хотя бы один machine-to-machine сценарий (service account/token).

  • Доступен SSO для пользователей.

  • Секреты приезжают в pod’ы из источника автоматически (не нужно копипастить).

  • Проверена ротация секретов

  • Понятно, кто и откуда получает доступ к токенам и секретам 

Wave 2 — Data plane

Состав волны:

  • Развернут HA-кластер PostgreSQL

  • Развернут HA-кластер Redis

  • Развернуто S3-совместимое хранилище (Minio, Rook Ceph)

  • Backup/restore политики для statefull-компонентов

  • Готовы базовые ресурсы (storage class, retention)

Что считаем результатом:

  • Stateful-сервисы стабильны после restart/rollout

  • Backup и restore протестированы хотя бы на тестовом наборе

  • Секреты доступны не хардкодом, а через Wave 1

  • Latency и throughput остаются приемлемыми для dev-сценариев.

  • Есть базовые smoke-тесты записи и чтения данных.

Wave 3 — ML stack

Что сюда входит:

  • Развернут реестр моделей (MLflow, например)

  • Развернут компонент работы с фичами (Feast)

  • Развернут компонент сервинга моделей (Ray)

  • Развернут компонент оркестрации (типа Airflow)

  • Развернут компонент работы с экспериментами (типа JupyterHub)

Что считаем результатом:

  •  Появляется минимальный сквозной ML-сценарий:

    • Аутентификация пользователя/сервиса

    • Регистрация эксперимента/артефакта

    • Чтение/запись фичи

    • Выполнение вычисления/jobs

    • Видимость логов/метрик/трейсов

  • Есть rollback-сценарий для каждого ключевого ML-компонента.

  • Совместимые версии зафиксированы (chart/app/SDK/docker).

Как не превратить wave в бюрократию

Как обычно, любой хороший подход можно взять и превратить во что-то неповоротливое и бюрократизированное. И wave — не исключение.

Три правила, которым стоит следовать, чтобы этого не произошло:

  1. В каждой волне держите небольшой набор обязательных smoke-тестов

  2. Перед переходом к следующей волне фиксируйте минимальный DoD, достаточный для текущего этапа

  3. Любой ручной hotfix в кластере сразу оформляйте в Git, иначе со временем теряется смысл Wave+GitOps

Если совсем упрощать — придерживайтесь «принципа достаточности». И всё.

Итог простыми словами

Если речь идёт не о production-кластере, то первый подход (сначала поднимаем весь стек, потом дорабатываем) вполне может быть оправдан. Потому что здесь куда дешевле поймать конфликты чартов/операторов/CRD и реальную топологию, а ещё нет давления prod-SLO и внешних пользователей. 

Второй же подход в чистом виде на dev зачастую избыточен: не всегда есть смысл доводить каждый компонент до production-ready-состояния, достаточно уровня «команда может стабильно работать с сервисом».

Интеграционный риск всё равно остаётся: даже хорошо настроенные по отдельности компоненты могут неожиданно начать конфликтовать уже в составе общей системы.

На практике лучше всего у нас сработал подход Wave + GitOps: платформа развивается слоями, каждый слой даёт работающий результат, а изменения фиксируются в Git.

При этом в отдельных случаях всё равно приходится делать вертикальные срезы.

Например: если ML-инженерам нужен JupytherHub, задача меняется: важно определить минимальный набор зависимостей, который позволит начать работу уже сейчас. Именно баланс между «достаточно хорошо» и «не переусложнять раньше времени» оказался для нас самым практичным подходом.

Интересно, как подобные платформы разворачиваются у вас: через вертикальные срезы, полноценные платформенные волны или как-то иначе?

Буду рад, если поделитесь опытом и советами в комментариях.

Полезные ссылки

  • DORA — исследования и метрики поставки ПО; опора для мысли про «короткую обратную связь» и частые интеграции vs редкие крупные выкаты.

  • Accelerate (Forsgren, Humble, Kim) — книга, с которой исторически связана линия DORA; практики высокоэффективных команд доставки.

  • OpenGitOps — принципы GitOps (Git как источник правды, декларативность и т.д.); контекст к упоминаниям Flux и дисциплины изменений через репозиторий.

  • Flux — GitOps-контроллер для Kubernetes; соответствует стеку в статье (FluxCD).

  • CNCF Platform Engineering Maturity Model — зрелость платформы и поэтапное наращивание возможностей (волны, не «всё с первого дня»).

  • Google SRE Book — культура SRE и идея осмысленного Definition of Done (наблюдаемость, снижение рутины toil, runbook-подход к эксплуатации).