Kubernetes

CronJob は Pod をスケジュールする。オーケストレーションはしない。

スケジュールはすでに Kubernetes がやっています。できていないのは、ジョブ間の順序を表現すること、Pod が回収された後もログを残すこと、クラスタの外に手を伸ばすことです。Dagu は各ステップを Kubernetes Job として実行し、その周りにグラフと履歴と境界越えを足します。

各ステップが本物の Kubernetes Job。kubectl のラッパーではない
Pod のログが Dagu の実行履歴に残り、Pod より長生きする
kubeconfig の context で 1 つのワークフローが複数クラスタにまたがる
クラスタのステップと SSH・HTTP・承認を同じグラフに並べられる
01

CronJob にできないこと

CronJob は 1 つの Pod のスケジューラとしては良くできています。足りなさが出るのはバッチが 1 Pod を超えた瞬間で、どのチームも同じ 3 つにぶつかります。

  • CronJob 同士に依存関係がありません。順序は時刻のずらしで表現することになり、ジョブが伸びた最初の日に静かに壊れます
  • 履歴は既定で浅く、成功 3 件と失敗 1 件しか残りません。ログは Pod と一緒に消えるので、先週火曜の失敗を説明することはたいてい不可能です
  • startingDeadlineSeconds を設定しないまま 100 回連続でスケジュールを逃すと、CronJob はスケジュール自体をやめ、人が気づくまで止まったままになります
02

ステップが Job であり、ログが戻ってくる

Dagu はステップごとに単一コンテナの Job を作り、完了を待ち、Pod のログをストリームし、終了したコンテナの終了コードを使います。ログは実行履歴に書き込まれるので、Pod と Job が消えた後も残ります。

  • depends が順序をそのまま表現するので、抽出が失敗したらロードは動かず、空のデータを流し込みません
  • retry_policy は新しい Job を作り直してステップを再試行します。backoffLimit が同じ Pod を再起動するのとは別のことです
  • 既定で完了後に Job を削除するので、夜間のグラフが終わった Job の列を残しません

Kubernetes はコンテナのログを 1 本のストリームとして公開するため、このステップ型では stdout と stderr が混ざった単一のストリームとして届きます。

3 ステップのバッチを本物の 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

1 つのクラスタに収まらない仕事

ここは CronJob がどれだけ努力しても届かない領域です。CronJob は自分が属するクラスタにスコープされているからです。2 つのクラスタにまたがる、あるいはクラスタとオンプレのシステムにまたがるワークフローは、Kubernetes の中に置き場所がありません。

  • kubeconfig と context はステップ単位で指定できるので、ステージングと本番が同じマニフェストの 2 つのデプロイではなく、1 つのワークフローの 2 つのステップになります
  • オーケストレータ自身がクラスタのリソースではないため、クラスタのステップをレガシーホストへの SSH と SaaS API への HTTP の間に置けます
  • 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 は 1 バイナリでクラスタの外にも置けます。クラスタ間と境界越えのグラフが成立するのはそのためです
  • ワークフロー定義を Kubernetes オブジェクトとして、クラスタの他の部分と同じ RBAC と GitOps で扱いたいなら、それは Argo の設計であって Dagu の設計ではありません
05

このステップ型が意図的に持たないもの

Kubernetes ステップは意図的に狭く作られています。バッチステップのよくある形はきちんとカバーしますが、汎用のマニフェスト適用装置になろうとはしていません。

  • ステップあたり 1 コンテナ、コマンドは 1 つ。Pod や Job の spec をそのまま渡す経路はありません
  • Job の parallelism、completions、completion mode、success policy は公開していないので、インデックス付きジョブや並列ジョブは対象外です
  • restart_policy は設定できず、Windows 固有の設定、SELinux、AppArmor、proc_mount も利用できません

ドキュメントに載っていないフィールドは未対応だと考えてください。完全な Job spec が必要な処理は、コマンドステップから kubectl で適用するか、Argo に任せるほうが適切です。

FAQ

導入前によくある質問

Dagu はクラスタの中で動かす必要がありますか?

ありません。明示的な kubeconfig、通常の kubeconfig 読み込み規則、in-cluster 設定の順に解決するので、どちらでも動きます。外に置くとクラスタ間と境界越えのワークフローが可能になり、触る対象がすべてそのクラスタの中にあるなら中で動かしても問題ありません。

kubectl を呼ぶスクリプトを CronJob で回すのと何が違いますか?

その方式で得られるのは順序だけです。ラッパー Pod の終了コードはどの段階で失敗したかを隠し、リトライはスクリプト全体をやり直し、ログは区別のつかない 1 本の塊として Pod と一緒に消えます。Dagu はステップごとに Job を 1 つ作るので、各段階が自分の状態、自分のリトライ、自分の保存されたログを持ちます。

ステップ間でファイルシステムを共有しますか?

しません。各ステップは別の Job であり、したがって別の Pod です。ステップ間のデータ受け渡しは、オブジェクトストレージ、各ステップにマウントする永続ボリューム、小さな値ならステップ出力を使ってください。別々の Job の間で行うのと同じ考え方です。

実行を中断したとき Job はどうなりますか?

キャンセル、強制終了、タイムアウトの経路では cleanup_policy が keep でもクリーンアップが行われるため、止めた実行が Job を残しません。通常の動作では、完了時に Job を削除するのが既定です。

Argo Workflows の代わりに使うべきですか?

オーケストレータがクラスタの外にあることが有用な場合だけです。すべてのタスクが 1 つのクラスタ内のコンテナで、ワークフローを同じ RBAC と GitOps の下で Kubernetes オブジェクトとして扱いたいなら Argo のほうが合います。Dagu が向くのは、クラスタのジョブとホスト、API、人の承認が混ざるグラフです。

次のステップ

まず 1 本から始める。

Dagu を入れて、いま cron で動いているスクリプトを 1 本だけ YAML に移し、実際の実行履歴で判断してください。