🔂

Ubieのワークフロー移行とプラットフォームエンジニアリング

2025/01/30に公開

始めに

Ubieでプラットフォームエンジニア兼SREをしているonoteruです。

前回の記事では、テンプレーティングツール「ubieform」を使ったプラットフォームエンジニアリングと、マルチクラスタ構成への移行について紹介しました。昨年実施した以下の3つのインフラ移行の1つ目になります。

  • 単一のGKEクラスタからマルチクラスタ構成への移行
  • Argo Workflowからフルマネージドなワークフローへの移行
  • CloudSQLからスケーラブルで管理性の高いDB(AlloyDB)への移行

今回はその続編として、2つ目のArgo WorkflowからCloud Workflowsへの移行、つまり「フルマネージドなワークフローへの移行」について詳しく解説していきます。

この移行は、前回紹介したマルチクラスタ移行と同様に、Ubieのプラットフォームをより信頼性が高く、運用負荷の少ないものへと進化させるための重要な取り組みでした。特に、ワークフローエンジンをマネージドサービスへ移行することで、プラットフォームチームの運用負荷を軽減しつつ、開発者により安定した実行環境を提供することを目指しました。

本記事では、移行の背景となる課題から、具体的な実装の詳細、そして移行後に得られた効果までを紹介していきます。

背景

前回の記事にある通り、私たちはUbieのマイクロサービスを、新規作成した複数のGoogle Cloudプロジェクトの新たなGKEクラスタへと移行しました。しかし、この移行後も古いクラスタを完全に手放すことができない状況が続いていました。その主な理由が、Argo Workflowの存在です。

Ubieでは以前よりワークフローエンジンとしてArgo WorkflowをGKE上で利用しており、定期的なデータ処理やバッチ処理の実行基盤として運用してきました。そこで実行されるワークフローの中には、複数の事業領域にまたがってデータを処理するものが存在しています。例えば、マスターデータを定期的アップデートするワークフローでは、医療機関向けサービスとtoC向けサービスの両方のデータベースに対して書き込みを行う必要がある、といったものです。

理想的には、それぞれの事業領域に対応するクラスタ上にデータを書き込むワーカーを配置したいところですが、Argo Workflowはマルチクラスタ環境に対応していないため、この構成を実現することができませんでした。各クラスタに対して個別にArgo Workflowを導入し連携させる方法も考えられますが、構成が複雑になりすぎたり、コスト効率がよくないといった理由で採用しませんでした。

そのため、マイクロサービスが新たなクラスタに移行された後も、ワークフローは旧Google Cloudプロジェクト上の旧クラスタで稼働したままになっていました。このままではシステム全体を完全なマルチプロジェクト・マルチクラスタ構成にすることはできません。そこで、ワークフローをマイクロサービスと同様のマルチプロジェクト構成にし、旧クラスタの完全な削除を目的とした移行プロジェクトが立ち上がりました。

マイクロサービスのクラスタ移行直後のアーキテクチャ
マイクロサービスのクラスタ移行直後のアーキテクチャ

また、Argo Workflowには開発者視点での課題もありました。Argo Workflowは、並列実行、依存関係の制御、再試行ポリシーなど、豊富な機能を備えたワークフローエンジンです。各種カスタムリソースを組み合わせることで、複雑な処理フローを表現できます。これらのメリットがある一方で、アプリケーション開発者は、ビジネスロジックの実装に加えてArgo Workflow特有の概念や設定方法を学ぶ必要があり、ワークフローを作成・理解するのに一定の認知負荷がかかっていました。

さらにOSSであるArgo Workflowは定期的なバージョンアップが必要で、そこに対しても一定の運用工数を負担しなければなりません。

これらの課題を解決するため、新たなワークフローエンジンへの移行を検討しました。検討の結果、以下の理由からGoogle Cloud Workflowsが最も要件に合致していると判断しました。

  • ワーカーとなる個別のGKEやCloud Runを呼び出す形式なので、マルチプロジェクト・マルチクラスタの現状構成と相性がいい
  • Google Cloudの各種リソースと組み合わせてワークフローを構成するので、既存の知識を使いまわせる
  • フルマネージドなサービスなので、それ自体の運用負荷がない

新しいワークフローアーキテクチャ

Cloud Workflowsへの移行にあたり、以下のようなアーキテクチャを採用しました。

新ワークフローのアーキテクチャ
新ワークフローのアーキテクチャ

基本的な構成として、Cloud Workflowsの各ステップで各種Google Cloudサービスを呼び出す方式を採用しています。Cloud Workflowsは異なるプロジェクトのサービスを呼び出すことができるため、各ステップの処理を必要に応じて適切なプロジェクト上で実行することが可能になりました。

ステップとして呼び出せるサービスは以下の3種類です。

  • Cloud Run jobs
  • Cloud Run services
  • Cloud Workflows

基本的にはステップとしてCloud Run jobsを利用するものがほとんどです。ただし、jobsは終了コード以外の処理結果を呼び出し元に返せないため、なんらかの値を返したい場合はservicesを呼び出すことで対応しています。3番目のCloud Workflowsは、ワークフローで連鎖的に他のワークフローをキックしたい場合に使います。

また、ワークフローの起動方法として以下の3つのパターンを用意しました

  1. Cloud Consoleやgcloud CLIによる手動トリガー
  2. Cloud Schedulerと連携した定期実行
  3. Pub/Subを使用したイベントドリブンな実行

この構成により、マルチプロジェクト環境での柔軟なワークフロー実行が可能になり、かつGoogle Cloudのマネージドサービスを活用することで運用負荷を最小限に抑えることができました。

なお、Cloud WorkflowsはステップとなるコンテナをGKEのPodとして作成することもできます。しかし、この機能はまだプレビューの段階であり、安定性に懸念があったため採用を見送りました。GAになった際はぜひ検証したいと考えています。

実装:ubieformによるワークフロー定義の自動生成

前回の記事で紹介したように、私たちはubieformを使って様々なインフラ設定を自動生成し、主にCloud Deployでサービス設定をデプロイしています。Cloud Workflowsへの移行にあたっても、同様のアプローチを採用しました。

Terraformによるワークフロー管理

ステップとなるCloud RunはCloud Deployでアプリケーションと同様にデプロイできますが、Cloud Workflowsと関連リソースは、Cloud Deployでのデプロイに対応していません。そこで、私たちはubieformを拡張し、Terraformの設定ファイルを生成できるようにしました。以下が、ubieformでワークフローを定義する際の設定例(抜粋)です。ubieformの詳細については、前回の記事を参照してください。

workflow: {
  jobs: [
    {
      name: "sample-job"
      gcp_service_account: "sample-{{.Env}}@example.iam.gserviceaccount.com"
      main_container: {
        # 実行するコンテナイメージのパスとタグ。appで利用しているイメージをそのまま参照できる
        image_path: app.main_container.image_path
        image_tag: app.main_container.image_tag
        command: ["/sample"]
        config_names: ["sample-env"]
        secret_names: ["sample-secret"]
      }
    },
  ]
  workflow_definitions: [
    {
      name: "example-wf"
      notifications: [
        # Slackへの結果通知設定
        {
          slack: {
            channel: "#notify-workflow-{{.Env}}"
          }
        }
      ]
      steps: [
        {
          name: "prepare"
          job: {
            name: "sample-job"  # jobsで作ったjobを参照する
            override: {
              args: [
                "prepare-data",
              ]
            }
          }
        },
        {
          name: "process"
          job: {
            name: "sample-job"  # jobsで作ったjobを参照する
            override: {
              args: [
                "process-data",
              ]
            }
          }
        },
      ]
      # 定期実行設定
      schedulers: [
        {
          name: "everyday"
          cron: "0 18 * * *"
          time_zone: "Asia/Tokyo"
        }
      ]
    }
  ]
}

jobsフィールドに定義したものからはCloud Runが、workflow_definitionsに記述したものからは以下のTerraformリソースが作成されます。

  • google_workflows_workflow
  • google_cloud_scheduler_job
  • google_pubsub_topic
  • google_eventarc_trigger
  • 各種Service Accountとiam_memberによるロールの紐付け

ワークフローの実行時には、jobsで定義したCloud Runリソースが順番に呼び出されていきます。各ステップでは引数をオーバーライドできるため、同じCloud Run jobsを異なるパラメータで再利用することも可能です。さらに、他のubieform configで定義されたCloud Runも呼び出せるため、プロジェクトをまたいだワークフローも実現できます。

また、Slackへの通知設定や定期実行のスケジュールなど、運用に必要な設定も同じファイルで管理できます。定義されたワークフローは自動的に社内サービスカタログのUbieHubに登録され、Google CloudコンソールのURLと共に一覧化されます。

デプロイ

ステップとして使うCloud RunはCloud Deployでデプロイできますが、GKEとCloud Runを同じパイプラインでデプロイすることはできません。そのため、ワークフローを含むサービスのデプロイには、2つの異なるパイプラインが必要になります。

  1. サービス本体(GKEのマニフェストなど)をデプロイするパイプライン
  2. ワークフローのステップ(Cloud Run)をデプロイするパイプライン

ubieformは、ワークフローが定義されたサービスに対して、自動的に2つ目のパイプラインを作成します。これらのパイプラインは、デプロイイベントを通して連携して動作します。例えば、本番環境へのデプロイ時には開発者の承認が必要ですが、サービスのパイプラインで承認すれば、ワークフローのパイプラインも自動的に承認され、デプロイが実行されます。

さらに、この構成でCloud Workflowsを使用する際の重要なポイントとして、依存関係の管理があります。以下の順序で各リソースを作成する必要があります。

  1. Cloud RunのSAがSecret Mangerのシークレットを取得するための権限付与
  2. ステップとして実行するCloud Runの作成
  3. それらのJobsを呼び出すWorkflowの作成

それぞれ前のステップが完了していないとデプロイに失敗するため、ubieformが生成するPRに関連するイベントを使いタイミングを制御しています。

コンポーネントが増えてくると内部的な実装は複雑になりますが、開発者から見たインターフェースはシンプルに保つことを重視しました。開発者は従来通りubieform configを記述し、サービスのデプロイを承認することで必要なリソースがデプロイされます。

移行

ワークフロー移行はマイクロサービスの時と同様、チームメンバーで分担し既存の設定をubieform configとして書き直すことで行いました。その中で直面した課題と対処法をいくつか紹介します。

Argo Workflowとの違い

Argo Workflowの設定をCloud Workflowsとして(ubieformを介して)書き直すわけなので、どうしても機能の違いから素直に移行させるのが難しいワークフローがありました。

Argo WorkflowとCloud Workflowsの重要な違いとして、Argo Workflowはスクリプトの実行とステップ間でのボリューム共有が簡単という点が挙げられます。例えばちょっとした処理をして次のステップにファイルを渡したい時を考えてみましょう。Argo Workflowではbashコンテナを用いてスクリプトを書き、共有ボリュームにファイルを書き込むことができます。

例えば、以下はArgo Workflowで$gitref変数の値を変換する実装例(抜粋)です。

spec:
  volumes:
    - name: shared-volume
      emptyDir: {}
...
  templates:
    - name: write-script
      script:
        image: bash:5.1
        command: ["bash"]
        volumeMounts:
          - name: shared-volume
            mountPath: /mnt/shared
        source: |
          result=$(echo "$gitref" | sed 's/[^a-zA-Z0-9_]/_/g')
          result=$(echo "$result" | sed 's/^_*//; s/_*$//')
          echo $result > /mnt/shared/shared-file.txt

    - name: read-script
      script:
        image: bash:5.1
        command: ["bash"]
        volumeMounts:
          - name: shared-volume
            mountPath: /mnt/shared
        source: |
          cat /mnt/shared/shared-file.txt

一方、Cloud Workflowsではワークフロー定義のステップに任意のスクリプトを書くことはできず、代わりに組み込み関数を使って処理を実現します。またローカルのボリュームはサポートされていないため、値やファイルを次のステップに渡したい時は、共有の変数に割り当てるか、Cloud Storageのような外部ストレージを使う必要があります。

上のArgo Workflowと同様の処理をCloud Workflowsでの実装すると以下のようになります。

steps:
  - process-gitref:
      assign: 
        - sanitized_gitref: ${text.replace_all_regex(text.replace_all_regex(gitref, "[^a-zA-Z0-9_]", "_"), "^_*|_*$", "")}

これはある意味Cloud Workflowsがサーバーレスのフルマネージドサービスであることの代償とも言えます。Cloud Workflowsは他のGoogle Cloudサービスをconnectorで呼び出し、それのオーケストレーションに徹しているからです。なお、Google CloudにはGCEを使うバッチ処理基盤としてBatchが提供されていますが、Argo Workflowはどちらかというとそれに近いものだと言えます。

さて、今回の移行対象となったワークフローの中には、ステップ間のボリューム共有によりファイルの受け渡しをしていたものが少数ながら存在していました。それらは上述の理由により素直にCloud Workflowsに変換できないため、Cloud Storageを使ってデータを受け渡すような修正を入れつつ移行を進めました。

ステップのログへジャンプしたい

Cloud Workflowsの管理画面からは、各ステップで実行されるCloud Run jobsのログを直接確認することができません。ログを確認するためには、ステップで使われているCloud Run jobsのnameと実行IDをCloud Loggingでフィルタリングする必要があります。これは開発者の利便性という観点で大きな課題でしたが、以下のような工夫で対応しました。

Cloud Run jobsの呼び出しでは実行IDが返却され、特にジョブが失敗した場合は適切にフィルタリングされたCloud LoggingのURLも一緒に返されます。この仕様を活用し、ubieformでは各処理ステップの直後にログへのリンクを出力するステップを自動挿入するようにしました。これによりCloud Workflowsのコンソールから直接Cloud Runのログへジャンプできるようになりました。

Cloud Run jobsの実行ログへのリンクが出力される
WorkflowsのコンソールでCloud Run jobsの実行ログへのリンクが出力される

このようにubieformによる抽象化で、クラウドサービス自体に不足している機能を補うことができました。とはいえ将来的にはCloud Workflows自体がCloud Runのコンソールへのジャンプ機能を提供してくれることを期待しています。

移行後

移行プロジェクトの完了により、全てのワークフローが新しいプロジェクト上のCloud Workflowsへと移行されました。これにより、長年運用されてきた旧GKEクラスタで実行される処理が完全になくなり、クラスタごと削除することができました。結果として全てのコンピューティング基盤がGKE Autopilotとなり、Standardクラスタのメンテナンスや互換性の考慮から解放されました。

また、開発者は以前までArgo Workflowの複雑な設定を理解する必要がありましたが、今ではubieformの直感的な設定だけでワークフローを作成できるようになりました。これにより、開発者は本来注力すべきビジネスロジックの実装により多くの時間を割けるようになっています。

まとめと次回について

Cloud Workflowsへの移行を通じて、私たちは以下のような成果を得ることができました。

  1. 運用負荷の削減:Argo Workflowの運用・保守から解放され、プラットフォームチームはより価値の高い業務に注力できるようになりました。

  2. マルチクラスタ対応:Cloud WorkflowsとCloud Runを活用することで、適切なプロジェクト上で処理を実行できるようになり、古いクラスタを完全に廃止することができました。

  3. 開発者体験の向上:ubieformによる設定の自動生成により、開発者はワークフローの本質的な処理に集中できるようになりました。

次回は、インフラ移行プロジェクトの最後の取り組みとなる「AlloyDBへの移行」について解説する予定です。

Ubie テックブログ

Discussion