1
/
5

Argo Workflows: 推薦基盤向けワークフローエンジンを Kubernetes で運用して1年経ったので振り返る

Photo by Jon Tyson on Unsplash

こんにちは。Matching チームの笠井(@unblee)です。

この記事では、おおよそ1年以上前に私が新卒入社して Infrastructure チームで一番最初に取り組んだ Workflow Engine の導入プロジェクトについての振り返り・供養をしようと思います。

記事内で触れている情報・判断は全て1年以上前の時点の社内外の状況に基づいていることを前提としているため現在の最新の情報とは異なることがあります。この前提を承知した上でお読みいただければ幸いです。

Agenda

  1. 問題意識
  2. 技術選定
  3. PoC
  4. 本番環境への導入とその障壁の解決
  5. 振り返り
  6. まとめ

問題意識

Wantedly では以前から推薦基盤へ力を注いでおり、Wantedly Visit におけるユーザと企業の理想のマッチングを実現するための推薦システムの改良や、データサイエンスを活用したプロダクト開発に責任を持つ専門のチーム(現在は Matching Squad)を立ち上げて開発を行っています。

この記事の主題である Argo Workflows 導入のきっかけは、1年以上前に当時の推薦基盤チーム(Recommendation Squad という名前だった)が構築していた基盤で使われる Batch Job の複雑化を解決するためでした。

Wantedly のインフラには以下のような特徴があります。

  • Kubernetes をサービス運用基盤として利用している
  • マイクロサービスアーキテクチャを実践していて、小さな機能を持ったリポジトリが複数存在している
  • Kubernetes では Namespace という概念によって操作権限等を分離していて、Wantedly では1リポジトリ1 Namespace という関係性を持っている

このような前提の下、当時の推薦基盤は以下のような状況でした。

  • 推薦基盤を構成している Batch Job は Kubernetes 上の CronJob によって実行されている
    • CronJob 自体の詳細な説明はしないが、この記事においては一般的な Linux における cron と同じように指定した時刻・間隔で指定したコマンドを実行するものであるとざっくり捉えて欲しい
  • 1つの推薦ワークフローを構成する CronJob であっても複数のリポジトリで分散して管理されている
  • CronJob 間の依存関係は実行スケジュールの絶対時刻を前後にずらすことによって表現・管理されている
  • CronJob 間の依存関係がコード上で管理されていない && どこのリポジトリで管理されているか明確では無い

推薦基盤のスケールが小さい状況では問題無かったのですが、徐々にスケールが大きくなる、複雑性が増してくると以下のような問題が表面化してきました。

  • マイクロサービス間を超えた定期実行される Job の依存関係を表現する方法がない
    • データ量が増加した等で1つの Job の実行時間が延長した場合、それに合わせて後続の Job の実行開始時刻を全てずらす必要がある
    • Job 間に新しい Job を挿入する必要がある場合、後続の Job の開始時刻をどれだけの時間ずらす必要があるのか1つ1つ検証する必要がある
    • 各 Job の実行のトリガーが1つ前の Job が完了したかどうかではなく、指定された時刻になっているため1つ前の Job が失敗していたとしても(Job内に失敗を検知する仕組みがない場合)後続の Job が実行され予期しない動作が行われる可能性がある
  • 依存関係がリポジトリに設定ファイル・コードとして保管されていない(暗黙知化)
    • CronJob 単体ならマニフェストファイルがリポジトリに存在しているが、その実行順序や依存関係の概要は明確に記録されたファイルが存在しないため作った本人または近しい関係者にしかわからない
  • 依存関係を俯瞰出来ない
    • 依存関係が暗黙知となっているので、依存関係においてどこまで成功していてどこから失敗しているのかが一見してわからない

以上の問題を解決するために、Batch Job の依存関係の適切な実行とコード上での明示的な管理の実現を狙って Workflow Engine の導入を検討し始めました。

技術選定

世の中に Workflow Engine は数多く存在します。それらの中から Wantedly のユースケースに寄り添ったものを絞り込むための要件定義と、実際に動かしてみないと分からないこともあるので動作確認を当時 Wantedly に来ていたインターン生と最初に取り組みました。

社内へのヒアリングによって導入においては以下の要件があることがわかりました。上にある要件ほど優先度が高いものになります。

  • Workflow 駆動で実行
    • 依存関係ある Job が定義できる
  • モニタリングログの可視化
    • 通知
    • UIイケてる(依存関係がわかる)
  • スケーラブルなエンジンかSPoF にならない設計になっているか
    • k8s が死んだら終わり => OK
    • 1 台の argo サーバーが死んだら終わり => NG
    • データがどこに置かれるのか
    • あちこち移動させてやる必要があるといずれ破綻する(AWS <> GCP の移動等)
  • エンジニア like で使い勝手が良いか
    • リトライとかがやりやすい、個別でできるか(全部やり直し?個別で実行できる?)
    • Job の追加がやりやすいか。現在ウォンテッドリーでは、Job の定義を CronJob でやっている
    • 依存関係(namespace)が増えたときに実行できるのか?依存関係の量が増えても同じ使い方ができるのか?
    • namespaces が増えることで複雑にならないか?
    • 記法が標準的であるか?(独自記法ではない)
    • Job の定義が1つのファイルに全て書かなければならない(中央集権)のはエンジニアライクではないので、複数から参照される Job をうまく import できる仕組みがあるか
  • Event 駆動で実行イベントを検知して実行する Job が定義できる

以上の要件から、ざっくりドキュメント等の調査だけをして最低限要件を満たしていそうだと感じた以下の Workflow Engine を導入候補として検証を進めました。

  • マネージド
    • GCP Cloud Composer(Apache Airflow)
  • アプリケーションライブラリ
    • Luigi
  • ノンマネージド
    • Argo Workflows

検証内容としては、推薦基盤で実行される実際の Batch Job の依存関係を各プロダクトの表現方法で記述し、検証環境で実際に実行してみるということを行いました。

検証の結果、各 Workflow Engine が要件を満たしているかどうかをチェックしたところ以下のような結果になりました。

この結果を社内のインフラエンジニアや推薦基盤チームといったステークホルダに共有し、最終的にはWantedly の運用基盤である Kubernetes のエコシステムの上に乗りつつ運用知見の蓄積・応用がしやすい Argo Workflows を導入することに決まりました。

Proof of Concept(PoC)

PoC では当時推薦チームが管理している以下の CronJob の依存関係を Argo Workflows に置き換えるということをやりました。画像は載せられませんが、元々はホワイトボードで Job 名とその実行開始時刻の依存関係を管理していました。

本番環境への導入とその障壁の解決

社内環境とユースケースにフィットさせるために小さなツールを自作しつつ、以下のようなアーキテクチャで Workflow を実行しています。

Argo v2.5.0 からは Cron Workflows があるのでそれを使えば良いと思いますが、当時は無かったので Workflow マニフェストを ConfigMap に書き出してそれを CronJob から argo-cli コマンドで叩くという運用をしました。

例えば、以下のようなマニフェストファイルになります。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: argo.argo-workflow.cm
  namespace: argo
data:
  argo.argo-workflow.wf: |
    apiVersion: argoproj.io/v1alpha1
    kind: Workflow
    metadata:
      generateName: argo-workflow-
    spec:
      entrypoint: argo-workflow
      imagePullSecrets:
        - name: xxx
      templates:
      - name: microservice-A
        container:
          image: registry/wantedly/microservice-A:latest
          command:
            - awesome
            - command
          envFrom:
            - secretRef:
                name: microservice-A.dotenv
      - name: microservice-Z
        inputs:
          parameters:
          - name: jobname
        container:
          image: registry/wantedly/microservice-Z:latest
          command:
            - amazing
            - command
          envFrom:
            - secretRef:
                name: microservice-Z.dotenv
      - name: argo-workflow
        dag:
          tasks:
          - name: microservice-A-A
            template: microservice-A
          - name: microservice-A-B
            dependencies: [microservice-A-A]
            template: microservice-A
          - name: microservice-Z-C
            dependencies: [microservice-A-A]
            template: microservice-Z
          - name: microservice-Z-D
            dependencies: [microservice-A-B, microservice-Z-C]
            template: microservice-Z
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: argo.argo-workflow.cj
  namespace: argo
spec:
  schedule: "0 20 * * *"
  successfulJobsHistoryLimit: 10
  failedJobsHistoryLimit: 3
  startingDeadlineSeconds: 300
  jobTemplate:
    metadata:
      name: argo.argo-workflow.cj
      namespace: argo
    spec:
      backoffLimit: 0
      template:
        metadata:
          name: argo.argo-workflow.cj
          namespace: argo
        spec:
          serviceAccountName: argo-wf-creator
          restartPolicy: Never
          containers:
            - name: exec-workflow
              image: argoproj/argocli:vX.Y.Z
              command:
                - argo
                - submit
                - /argo-wf/argo.argo-workflow.wf
              volumeMounts:
                - name: wf-manifest
                  mountPath: /argo-wf/
          volumes:
            - name: wf-manifest
              configMap:
                name: argo.argo-workflow.cm

見て分かると思うのですが、かなり構造が入り組んでいて人間が手で編集するのは現実的ではありません。導入当初はテンプレートをコピペして必要な部分を編集するようなことをしていたのですが、複雑さを隠蔽し切れていませんでした。そこで推薦基盤チームの @rerost がテンプレートとパラメータファイルを組み合わせることで Workflow マニフェストファイルを生成する rerost/dagg という小さなツールを作ってくれました。これを使うと複雑なマニフェストが以下のようにユーザが興味のある部分だけ編集すれば良いものになりかなり生産性を向上させる事が出来ました。

name: argo-workflow
option:
  team: argo
  schedule: "0 20 * * *"
jobs:
  - name: microservice-A-A
    commands: [awesome, command]
    option:
      repo: microservice-A
  - name: microservice-A-B
    commands: [awesome, command]
    dependencies: [microservice-A-A]
    option:
      repo: microservice-A
  - name: microservice-Z-C
    commands: [amazing, command]
    option:
      repo: microservice-Z
    dependencies: [microservice-A-A]
  - name: microservice-Z-D
    commands: [amazing, command]
    option:
      repo: microservice-Z
    dependencies: [microservice-A-B, microservice-Z-C]

また、Wantedly のインフラの特徴に書いたように推薦基盤用の Batch Job の具体的なコードは複数のリポジトリに分散しておかれています。Secret は Namespace を超えて参照することが出来ないので Argo を実行している Namespace に必要な CronJob を含むリポジトリに対応する Namespace の Secret を複製するツールが必要だと考えました。そこで作成したのが、wantedly/rigger という小さな Kubernetes の Custom Controller です。これによって複数のリポジトリ(Namespace)にまたがって Batch Job のコードが分散している Workflow の実行が可能となりました。詳細は以前私が発表した資料を参照してください。(この資料を発表したとき Argo のメンテナの方からより適切な方法を教えてもらったので社内でもいずれ rigger は使わなくなるかもしれません)

  • rerost/dagg
    • マニフェストファイルのジェネレータ
    • Argo Workflows のマニフェストファイルが複雑で手動で書くのは大変なので、必要な Placeholder だけ定義してそこに値を流し込むということをしている
  • wantedly/rigger
    • 複数の Namespace に存在する Secret(トークンやパスワードのような機密情報)を1つの特定の Namespace に集約するためのソフトウェア
    • Argo で扱う Workflow は複数の Namespace に所属する Job を利用するので、それらの Job が必要とする Secret を1つの Argo を実行している Namespace に集約する必要がある

これらのツールを使って以下のような流れで Workflow を実行しています。

  1. Workflow のマニフェストファイルテンプレートを用意する(これはリポジトリ内に事前に作成されている)
  2. Argo で実行したい Container Image とコマンド、依存先の Job の内容記述したを DAG file と呼ばれる dagg のテンプレート内パラメータを設定するための YAML ファイルを作成する
  3. マニフェストファイルテンプレートと DAG file を指定して dagg を実行し、Workflow マニフェストとそれを Kick するための CronJob を Kubernetes 上に作成する
  4. 事前に rigger が複製していた Secret をマウントして Workflow を実行する Job を Kick
  5. Workflow が実行される

振り返り

プロジェクト

プロジェクト進行では現在も意識している基盤チームとアプリケーションチームでプロジェクトをうまく進める方法について学べたと思っています。

  • ステークホルダーと目標・互いに何をして欲しいかなどの期待値の一致
  • ユーザからのフィードバックループを小さく早くする
  • 目的を見失ったり間違った方向に進まないようにプロジェクトの進行やスケジュールなどをステークホルダーへ共有・透明性を担保する
  • MVP のゴールとそこに至るまでのクリティカルパスを明確にする

実際の効果

当初挙がっていた問題は Argo Workflows の導入によって全て解決することが出来ました。

  • マイクロサービス間を超えた定期実行される Job の依存関係を表現する方法がない
    • Argo Workflows によって1つ前の Job の終了をトリガーに Job を実行することが出来るようになった
    • 途中の Job が失敗したら後続の Job が実行されなくなった
    • 複数の Namespace の Job を含む Workflow が実行できるようになった
  • 依存関係がリポジトリに設定ファイル・コードとして保管されていない(暗黙知化)
    • Argo Workflows のマニフェストファイルに依存関係を書くようになった
  • 依存関係を俯瞰出来ない
    • Argo Workflows の Web UI によって Workflow 内のどの Job が失敗したのか分かるようになった

起こった問題

実際に運用してみて起こった問題として具体的には以下のことが挙げられます。

  • Argo Workflows のバージョンアップが出来ていない
    • 当時社内で Kubernetes 上で運用するコンポーネントの管理体制がまだ整っていなかった
    • 社内で導入した時点から現在の最新バージョンに至る過程で Argo Workflows のアーキテクチャ自体が変更されるポイントが存在していて、アップデート作業に対するコストが上がっている
  • アラートのオオカミ少年化
    • Argo には Prometheus 形式のメトリクスを配信するエンドポイントが存在しそこから Datadog にメトリクスを送信、問題があればアラートを鳴らすという運用をしている
    • Argo Workflowsの実行を管理する workflow-controller という Custom Controller がたびたび再起動するか何かしらのタイミングで過去の Workflow の失敗をメトリクスとして出力してしまうということに運用してから気づいた
    • 通常のアラートに混じって過去に対応した無意味なアラートが鳴るようになり、価値のある情報では無くなってしまった

まとめ

1年以上前のプロジェクトについて振り返ってみました。当時はまだ新卒入社したばかりで最初のプロジェクトがこれだったので、プロジェクト進行とか初めてのことばかりで苦労していた記憶しかなかったんですが、約3ヶ月前に Infrastructure チームから推薦基盤(Matching)チームに異動して今改めて振り返ると、現在も Argo Workflows によって推薦基盤が構成されていてもう Argo Workflows の無い世界は想像出来ないので、とても重要でインパクトのあるプロジェクトだったんだなと振り返ったことで改めて実感しました。反省点も色々ありますが、このプロジェクトで学んだことが自分の仕事の基礎になっていると実感しているので、最初に取り組めてとても良い経験になりました。

Infrastructure チームや推薦基盤(Matching)チームではインパクトのある面白い取り組みでどんどん攻めていくので興味のあるエンジニアの方や学生の方がいれば気軽に話を聞きに来てください!

Wantedly, Inc.'s job postings
16 Likes
16 Likes

Weekly ranking

Show other rankings