Kubernetes

CronJob планирует под. Оркестрации в нём нет.

Kubernetes уже умеет планировать. Чего он не делает — не выражает порядок между задачами, не сохраняет логи после сборки пода и не дотягивается до чего-либо за пределами кластера. Dagu выполняет каждый шаг как Kubernetes Job и добавляет вокруг граф, историю и пересечение границ.

Каждый шаг — настоящий Kubernetes Job, а не обёртка вокруг kubectl
Логи пода попадают в историю запусков Dagu и живут дольше пода
Один рабочий процесс может охватывать несколько кластеров через контексты kubeconfig
Шаги кластера соседствуют с SSH, HTTP и согласованием в одном графе
01

Чего CronJob не делает

Как планировщик одного пода CronJob хорош. Пробелы проявляются, как только пакет становится больше одного пода, и все команды натыкаются на одни и те же три.

  • Между CronJob нет зависимостей. Порядок выражается угадыванием смещений по часам и тихо ломается в первый же день, когда задача выполняется дольше
  • История по умолчанию мелкая: сохраняются три успешных запуска и один неудачный, а логи исчезают вместе с подами. Объяснить сбой прошлого вторника чаще всего невозможно
  • Если пропущено более ста расписаний подряд, а startingDeadlineSeconds не задан, CronJob полностью прекращает планирование и ждёт, пока кто-нибудь это заметит
02

Шаги — это Job, и логи возвращаются

Dagu создаёт на каждый шаг Job с одним контейнером, дожидается его, транслирует логи пода и использует код возврата завершившегося контейнера. Логи записываются в историю запусков, поэтому остаются и после того, как под и Job исчезли.

  • depends выражает порядок напрямую, поэтому неудачное извлечение останавливает загрузку, а не кормит её пустотой
  • retry_policy повторяет шаг, создавая новый Job, — это не то же самое, что backoffLimit, перезапускающий под на месте
  • По умолчанию Job удаляется после завершения, поэтому ночной граф не оставляет за собой шлейф завершённых Job

Kubernetes отдаёт объединённый поток логов контейнера, поэтому для этого типа шага stdout и stderr приходят вперемешку одним потоком.

Пакет из трёх шагов как настоящие Kubernetes Job
# k8s-nightly-etl.yaml
schedule: "0 2 * * *"
max_active_runs: 1

kubernetes:
  namespace: batch
  service_account: dagu-runner
  resources:
    requests:
      cpu: "250m"
      memory: "512Mi"

steps:
  - id: extract
    action: k8s.run
    with:
      image: ghcr.io/example/extract:1.4.0
      command: extract --source warehouse
    retry_policy:
      limit: 2
      interval_sec: 120

  - id: transform
    action: k8s.run
    with:
      image: ghcr.io/example/transform:1.4.0
    depends: extract

  - id: load
    action: k8s.run
    with:
      image: ghcr.io/example/load:1.4.0
      resources:
        limits:
          cpu: "2"
          memory: "4Gi"
    depends: transform

handler_on:
  failure:
    run: ./scripts/page-oncall.sh

mail_on:
  failure: true
03

Работа, не помещающаяся в один кластер

Это та часть, которую CronJob не осилит никакими усилиями, потому что CronJob ограничен кластером, в котором живёт. Рабочему процессу, затрагивающему два кластера или кластер и локальную систему, попросту негде существовать внутри Kubernetes.

  • kubeconfig и context задаются на уровне шага, поэтому staging и production становятся двумя шагами одного процесса, а не двумя развёртываниями одного манифеста
  • Шаг в кластере может стоять между шагом SSH на устаревшем хосте и HTTP-вызовом к SaaS API, потому что сам оркестратор не является ресурсом кластера
  • Шаг human.task приостанавливает процесс ради типизированного согласования, не удерживая процесс во время ожидания, а затем продолжает к продуктивному шагу
Продвижение миграции между кластерами с шагом согласования
# k8s-promote-across-clusters.yaml
max_active_runs: 1

kubernetes:
  namespace: batch
  service_account: dagu-runner

steps:
  - id: migrate_staging
    action: k8s.run
    with:
      context: staging
      image: ghcr.io/example/migrator:3.2
      command: migrate up

  - id: verify_staging
    run: ./scripts/verify-schema.sh staging
    depends: migrate_staging

  - id: approve
    action: human.task
    with:
      prompt: Promote the migration to production?
      form:
        type: object
        properties:
          change_ticket:
            type: string
          confirmed:
            type: boolean
        required: [change_ticket, confirmed]
    depends: verify_staging

  - id: migrate_production
    action: k8s.run
    with:
      context: production
      image: ghcr.io/example/migrator:3.2
      command: migrate up
    depends: approve

  - id: record
    run: ./scripts/record-change.sh "${steps.approve.outputs.change_ticket}"
    depends: migrate_production

handler_on:
  failure:
    run: ./scripts/page-oncall.sh

mail_on:
  failure: true
04

Где Argo Workflows подходит лучше

Argo Workflows — родной для Kubernetes ответ на ту же задачу, и для большого класса работ именно он правильный. Разница в том, где живёт оркестратор.

  • Argo работает как CRD и контроллер внутри кластера: это преимущество, если всё оркестрируемое уже там, и ограничение, если нет
  • Dagu — один бинарный файл, который может находиться вне кластера, и именно это делает возможными межкластерные и трансграничные графы
  • Если определения процессов должны быть объектами Kubernetes под тем же RBAC и GitOps, что и остальной кластер, — это замысел Argo, а не Dagu
05

Чего исполнитель намеренно не предоставляет

Тип шага Kubernetes намеренно узкий. Он хорошо покрывает обычную форму пакетного шага и не пытается стать универсальным применителем манифестов.

  • Один контейнер на шаг и одна команда. Прямой передачи сырых спецификаций Pod или Job нет
  • parallelism, completions, completion mode и success policy у Job не предоставляются, поэтому индексированные и параллельные Job остаются вне области
  • restart_policy не настраивается, а специфичные для Windows параметры, SELinux, AppArmor и proc_mount недоступны

Если поле не описано для этого типа шага, считайте его неподдерживаемым. Работу, требующую полной спецификации Job, лучше применять через kubectl из командного шага или оставить Argo.

FAQ

Practical questions before adopting

Обязательно ли запускать Dagu внутри кластера?

Нет. Он определяет кластер сначала по явному kubeconfig, затем по обычным правилам загрузки kubeconfig, затем по внутрикластерной конфигурации, поэтому работает в обоих случаях. Запуск снаружи как раз и делает возможными межкластерные и трансграничные процессы; запуск внутри уместен, когда всё затрагиваемое находится в этом кластере.

Чем это отличается от CronJob, запускающего скрипт с kubectl?

Такой подход даёт только порядок: код возврата пода-обёртки скрывает, какой этап упал, повтор перезапускает весь скрипт, а логи представляют собой единый неразличимый поток, исчезающий вместе с подом. Dagu создаёт по одному Job на шаг, поэтому у каждого этапа свой статус, свой повтор и свой сохранённый лог.

Разделяют ли шаги файловую систему?

Нет. Каждый шаг — отдельный Job и, следовательно, отдельный под. Передавайте данные через объектное хранилище, постоянный том, смонтированный в каждый шаг, или через выводы шагов для небольших значений — так же, как между отдельными Job.

Что происходит с Job при отмене запуска?

Пути отмены, принудительного завершения и таймаута выполняют очистку даже при cleanup_policy со значением keep, поэтому остановленный запуск не оставляет Job. В обычной работе Job по умолчанию удаляется после завершения.

Стоит ли использовать это вместо Argo Workflows?

Только если вам полезно, что оркестратор находится вне кластера. Если все задачи — контейнеры в одном кластере и вы хотите, чтобы процессы были объектами Kubernetes под тем же RBAC и GitOps, лучше подходит Argo. Dagu уместен для графов, смешивающих задачи кластера с хостами, API и согласованиями людей.

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.