
Привет! Меня зовут Александр Егоров, я MLOps-инженер в Альфа-Банке, куда попал через проект компании KTS.
За свою карьеру я построил четыре ML-платформы (одна из которых сейчас в Росреестре) и развиваю с командой пятую. Параллельно учусь в ИТМО по направлению «Безопасность искусственного интеллекта».
В этой статье я немного покритикую Airflow и поделюсь нашей историей миграции на связку Argo Workflows и Argo CD. Spoiler alert: технические подробности и результаты в наличии.
Оглавление
Проблемы Airflow
Airflow — инструмент, который любят и ненавидят. Он изначально создавался как оркестратор для ETL-задач, но со временем его стали использовать и для обучения моделей, и для инференса, и как универсальный шедулер. Однако на масштабе сотен ML-моделей он начинает мешать больше, чем помогать.
Я выделяю три ключевых недостатка Airflow: масштабируемость, отсутствие Kubernetes-нативности и слабый GitOps. Обсудим подробнее каждый из них.
Масштабируемость
Первая проблема с масштабируемостью упирается в хранение DAG’ов. DAG’и хранятся внутри пода шедулера в отдельной директории. Это вполне удобно для локальных разработок, когда разработчик, дата-сайентист и дата-инженер втроём могут сделать по одному DAG’у для обработки данных, для трейна, для инференса и мониторинга.
Однако для больших команд, которые работают с сотнями моделей, держать все в одном репозитории невозможно. Версионирование ломается, обновления DAG’ов превращаются в ручную синхронизацию, а команды начинают мешать друг другу.
Вторая проблема масштабируемости — запуск множества подов. В Airflow для запуска подов используются такие сущности, как Spark-оператор и Pod-оператор. В результате при выполнении задачи создаётся довольно много контейнеров: сам Spark, воркер и дополнительный wait-контейнер, который отслеживает завершение джобы.
Более того, проблема с количеством подов проявляется уже на этапе установки самого Airflow. Даже для того, чтобы просто выполнять задачи по расписанию и запускать скрипты, требуется целый набор подов с несколькими контейнерами внутри: scheduler, webserver (в версии 3+ — apiserver), statsd, triggerer и другие.
Отсутствие Kubernetes-нативности
Чтобы запустить Spark Application или просто под, приходится использовать Python-обёртки (SparkOperator, KubernetesPodOperator
). На самом деле мы хотим объявить сущность в YAML и применить kubectl apply
. Вместо этого приходится держать «двойное описание»: Python-DAG + Jinja-шаблон манифеста. Это усложняет и разработку, и отладку.
Раньше дата сайентист просто объявлял в конфиге название модели, версию Python и другие параметры. Теперь же, чтобы превратить такой конфиг в под, нужно:
сформировать DAG на Python (что плохо вяжется с Kubernetes, где описания делаются в YAML или JSON);
встроить в DAG логику шаблонизации манифеста;
и только после этого получить корректный Spark Application.
Все это усложняет дебаг и добавление новых фич. Даже для того, чтобы просто указать ресурсы задачи, приходится:
обновлять библиотеку, которая обрабатывает конфиги;
вносить изменения в DAG и в Jinja-шаблон;
и только потом вносить это все в Spark Application.
В итоге получается подход, который сильно зависит от разных компонентов, и который очень сложно апгрейдить.
Слабый GitOps
Главная проблема — костыльная загрузка DAG'ов.
В Airflow есть возможность подтягивать DAG'и с гита. В чарте можно даже прописать несколько репозиториев, чтобы они скачивали даги в сам шедулер.
Однако на практике при установке на разных платформах у нас периодически с гитом что-то было не так, и контейнер с загрузкой DAG'ов падал. К тому же, у нас в принципе нет DAG'ов, которые хранятся в самих репозиториях, потому что они формируются на основе единого конфига, чтобы поддерживать общий формат и единый подход.
Сравнение с альтернативами
Когда мы решили отказываться от Airflow, встал вопрос, чего мы ждем от нового инструмента. Нам был нужен Kubernetes-нативный инструмент с декларативным управлением через YAML и GitOps-подходом. Изначально мы рассматривали Dagster, и Kubeflow.
Первый кандидат отсеялся сразу, потому что не подходил ни под один из наших критериев. Да, у Dagster более современный и приятный интерфейс, чем у продуктов Apache, и работает он чуть быстрее, и компонентов поменьше, но архитектурно он отличается от Airflow весьма условно: DAG’и все также на Python, не K8s-native.
Второй кандидат, Kubeflow, уже представляет собой целую платформу и формально подходит под наши требования: полностью K8s-нативен, поддерживает GitOps и позволяет описывать пайплайны и на Python, и на YAML. Но его тяжесть убивает: десятки компонентов, сотня CRD, долгие и хрупкие установки. Это монолит, маскирующийся под микросервисы.
Однако в процессе R&D по Kubeflow мы познакомились с его «подкапотной» технологией Argo Workflow. Оказалось, что этот инструмент не просто покрывает наши нужды, но еще и разворачивается максимально просто. В нем нет горы дополнительных компонентов: один контроллер занимается абсолютно всей логикой шедулинга и выдает метрики по отдельным DAG’ам. Есть и второй под — интефейс, однако мы можем им даже не пользоваться.

Как изменился пайплайн
Как я уже сказал, работать с несколькими сотнями репозиториев с моделями через Airflow было довольно грустно:
В каждом репозитории лежал конфиг с названием, версией Python и ресурсами.
Jenkins клонил репозиторий, Python-библиотека на основе конфига генерировала DAG и Kubernetes-манифесты.
Все это отправлялось в Airflow Scheduler, который через какое-то время отображал DAG в интерфейсе.
Проблема в том, что мы фактически дублировали Helm/Kustomize своими велосипедами на Python и Jinja. К тому же пайплайн был медленным: генерация, передача в Scheduler и переваривание DAG’а занимали до 10 минут.
После перехода на Argo CD пайплайн стал проще. Вот так он теперь выглядит для онлайн-моделей:

Jenkins по-прежнему клонит репозиторий.
Собирается окружение (conda + requirements.txt).
Архив кладётся в S3 для переиспользования.
Генерируется Argo CD Application, который объединяет общий Helm-чарт для моделей и индивидуальные values из репозитория модели.
Argo CD синхронизирует кластер с Git: сервис поднимается и поддерживается в актуальном состоянии.
Для batch-моделей добавляется Argo Workflows: Workflow/CronWorkflow описывают задачи, WorkflowTemplate/ClusterWorkflowTemplate позволяют переиспользовать шаги. Каждая задача запускается как отдельный под, логи собираются в интерфейсе и дублируются в S3.

В интерфейсе ArgoCD это выглядит примерно так:

Здесь, в примере с инференсом batch-модели, генерируются две сущности CronWorkflow. Первый — это сам инференс, второй — мониторинг, который периодически следит за работой модели. CronWorkflow по расписанию генерирует DAG’и, которые выполняют некоторые задачи.

Преимущество такого подхода в том, что для модели четко видны отдельные технические таски (в данном случае это alfa-rclone, kinit и dag-extra-params). Все эти таски вместе с инференсом крутятся в одном поде. Из интерфейса сразу понятно, какая модель запущена и какой у нее процесс.
Важно отметить, что Argo позволяет передавать параметры в Workflow. Это позволило нам сделать «режим дебага»: можно поднять контейнер с отладкой и подключиться к нему из VS Code. Это снимает необходимость постоянно коммитить правки и дебажить через print.
Суммарное время нового пайплайна составляет около 4,5 минут, из которых:
20 секунд уходят на шаги Jenkins без скачиваний;
около трех минут занимает подготовка окружения (при этом оно кэшируется, и его можно переиспользовать);
7 секунд скачивается архив из S3;
примерно одна минута уходит на распаковку.
Итого, новый K8s-native пайплайн без Python-шаблонизаторов ускорился более чем вдвое, и это с учетом подготовки окружения. Git стал единым источником правды, как мы и хотели изначально. Поддержка и дебаг стали проще благодаря единому Helm-чарту, понятным манифестам и интерактивной отладке. В результате количество инцидентов и тикетов от дата-саентистов и инженеров снизилось примерно на 60%.
Однако здесь я хочу отметить, что моя критика в сторону Airflow применима только к тем кейсам, когда работать приходится с большим количеством моделей. Для маленьких команд с десятком DAG’ов этот инструмент все еще остается разумным выбором. Но если у вас Kubernetes, сотни моделей и желание жить по GitOps, то проще, надежнее и логичнее будет сразу строить пайплайны на связке Argo Workflows и Argo CD.
Немного полезных манифестов: CronWorkflow и WorkflowTemplate
Вместо прощания я приведу два примера, приближенных к тому, что используем мы в проде, чтобы было понятнее, как выглядит описание пайплайнов в Argo Workflows. Лишний Helm-синтаксис из примеров был удален.
WorkflowTemplate
Мы вынесли в WorkflowTemplate общие шаги, которые встречаются в 90% моделей: загрузка архива окружения из S3, загрузка кода и запуск скрипта.
Пример
```yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: model-train-template
namespace: ml-pipelines
spec:
entrypoint: train-model
arguments:
parameters:
- name: model-name
value: "default-model"
- name: train-script
value: "train.py"
artifacts:
- name: model-code
path: /workspace/model
git:
repo: "https://github.com/your-org/your-model.git"
revision: "version_1"
- name: env-archive
path: /workspace/env # ← ВАЖНО: это будет РАСПАКОВАННАЯ директория!
archive:
none: {}
s3:
endpoint: s3.amazonaws.com
bucket: ml-artifacts
key: envs/default-env.zip
accessKeySecret:
name: s3-credentials
key: accessKey
secretKeySecret:
name: s3-credentials
key: secretKey
region: us-east-1
templates:
- name: train-model
inputs:
parameters:
- name: model-name
- name: train-script
artifacts:
- name: model-code
path: /workspace/model
- name: env-archive
path: /workspace/env # ← Здесь уже распакованная директория окружения!
container:
image: python:3.9-slim
command: ["/bin/bash", "-c"]
args:
- |
set -e
# Активируем окружение
conda activate /workspace/env/
# Запускаем обучение
python /workspace/model/{{inputs.parameters.train-script}}
env:
- name: MODEL_NAME
value: "{{inputs.parameters.model-name}}"
```
В данном случае мы используем функции Argo Workflow, которые позволяют нам скачать код модели нужной версии с Git напрямую, а окружение, созданное на предыдущем этапе, будет загружаться уже с S3 хранилища (как и Git-репозиторий).
Фишка Argo Workflow в том, что он умеет паковать директории с нужным уровнем компрессии и сохранять их в S3. Затем он также умеет их распаковывать. Делается это нативно и за буквально секунды. Нам не требуется использовать какие-либо PVC для хранения — все это можно реализовать прямо в оперативной памяти пода.
CronWorkflow
Теперь любой Workflow
или CronWorkflow
может ссылаться на этот шаблон через refTemplate
, не дублируя код:
Пример
```yaml
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
name: daily-fraud-retrain
namespace: ml-pipelines
spec:
schedule: "0 2 * * *" # каждый день в 02:00 UTC
concurrencyPolicy: "Forbid" # не запускать, если предыдущий ещё не завершён
startingDeadlineSeconds: 3600 # запустить в течение часа, если пропустили
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
workflowSpec:
entrypoint: run-model-via-template
# Переопределяем аргументы шаблона: параметры + артефакты
arguments:
parameters:
- name: model-name
value: "fraud-detection-daily"
- name: train-script
value: "retrain_daily.py --window=7d"
artifacts:
- name: model-code
path: /workspace/model
git:
repo: "https://github.com/your-org/fraud-detection.git"
revision: "main" # или тег, например: "v4.2.1"
- name: env-archive
path: /workspace/env # ← Argo автоматически распакует ZIP в эту директорию!
s3:
endpoint: s3.amazonaws.com
bucket: ml-artifacts
key: envs/fraud-env-v4.zip # ← ZIP-архив с Python-окружением
accessKeySecret:
name: s3-credentials
key: accessKey
secretKeySecret:
name: s3-credentials
key: secretKey
region: eu-central-1
templates:
- name: run-model-via-template
templateRef:
name: model-train-template # ← ссылка на WorkflowTemplate
template: train-model # ← имя шаблона внутри
```
Важно: CronWorkflow
— это CRD от Argo, не путать с Kubernetes CronJob
. Он работает поверх Workflow, а значит, поддерживает сложные DAG’и, параллельные ветки, артефакты, retry-логику и UI.
Такой подход позволил нам:
убрать дублирование кода: шаблоны переиспользуются десятками моделей;
упростить обновление: поменяли шаблон → все зависящие пайплайны получили фикс;
ускорить разработку: дата-сайентисты описывают только параметры, не трогая инфраструктурную логику.
Это и есть настоящий GitOps: декларативные манифесты, хранящиеся в Git, с контролем версий, review и автоматическим применением через Argo CD.
И небольшой совет напоследок: используйте ClusterWorkflowTemplate
, если шаблоны нужны в нескольких неймспейсах — это глобальная версия WorkflowTemplate
.