GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows. argo.. argo. DevOps.. argo. DevOps. helm.. argo. DevOps. helm. Kubernetes.. argo. DevOps. helm. Kubernetes. mlops.
GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows - 1

Привет! Меня зовут Александр Егоров, я 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 и другие параметры. Теперь же, чтобы превратить такой конфиг в под, нужно:

  1. сформировать DAG на Python (что плохо вяжется с Kubernetes, где описания делаются в YAML или JSON);

  2. встроить в DAG логику шаблонизации манифеста;

  3. и только после этого получить корректный 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’ам. Есть и второй под — интефейс, однако мы можем им даже не пользоваться.

Фактически Argo Workflows — это тот же движок, который использует Kubeflow. Мы решили взять его напрямую, без громоздкой обёртки.

Фактически Argo Workflows — это тот же движок, который использует Kubeflow. Мы решили взять его напрямую, без громоздкой обёртки.

Как изменился пайплайн

Как я уже сказал, работать с несколькими сотнями репозиториев с моделями через Airflow было довольно грустно:

  1. В каждом репозитории лежал конфиг с названием, версией Python и ресурсами.

  2. Jenkins клонил репозиторий, Python-библиотека на основе конфига генерировала DAG и Kubernetes-манифесты.

  3. Все это отправлялось в Airflow Scheduler, который через какое-то время отображал DAG в интерфейсе.

Проблема в том, что мы фактически дублировали Helm/Kustomize своими велосипедами на Python и Jinja. К тому же пайплайн был медленным: генерация, передача в Scheduler и переваривание DAG’а занимали до 10 минут.

После перехода на Argo CD пайплайн стал проще. Вот так он теперь выглядит для онлайн-моделей:

GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows - 3
  1. Jenkins по-прежнему клонит репозиторий.

  2. Собирается окружение (conda + requirements.txt).

  3. Архив кладётся в S3 для переиспользования.

  4. Генерируется Argo CD Application, который объединяет общий Helm-чарт для моделей и индивидуальные values из репозитория модели.

  5. Argo CD синхронизирует кластер с Git: сервис поднимается и поддерживается в актуальном состоянии.

Для batch-моделей добавляется Argo Workflows: Workflow/CronWorkflow описывают задачи, WorkflowTemplate/ClusterWorkflowTemplate позволяют переиспользовать шаги. Каждая задача запускается как отдельный под, логи собираются в интерфейсе и дублируются в S3.

GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows - 4

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

GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows - 5

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

GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows - 6

Преимущество такого подхода в том, что для модели четко видны отдельные технические таски (в данном случае это 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.

Автор: IAlexOps

Источник

Rambler's Top100