以下の内容はhttps://techblog.zozo.com/entry/release-pipeline-argo-events-workflowsより取得しました。


Argo EventsとArgo Workflowsの導入によるリリースパイプラインの改善

Argo EventsとArgo Workflowsの導入によるリリースパイプラインの改善

はじめに

こんにちは。グローバルプロダクト開発本部SREブロックの纐纈です。

弊チームでは、Kubernetes上で動作する4つのサービス(ZOZOMAT、ZOZOGLASS、ZOZOMETRY、お試しメイク)のリリースを自動化しています。これまでにArgo CDによるGitOpsやArgo Rolloutsによるカナリアリリースを導入してきました。

techblog.zozo.com

techblog.zozo.com

リリースパイプラインの全体像については以下の記事で紹介しています。

techblog.zozo.com

本記事では、このリリースパイプラインのトリガー方式を見直した取り組みについて紹介します。改善にあたり、Argo EventsとArgo Workflowsを活用しました。Argo Eventsはイベント駆動型の自動化フレームワークで、EventSourceで様々なイベントを受信しSensorで後続処理をトリガーできます。Argo WorkflowsはKubernetes上でDAG形式のワークフローを実行するエンジンです。

argoproj.github.io

argoproj.github.io

目次

リリースパイプラインの全体像

まず現在運用しているリリースパイプラインの全体像を説明します。

弊チームでは、アプリケーションコードを管理するサーバーリポジトリと、Kubernetesマニフェストを管理するKubernetesリポジトリを分離しています。サーバーリポジトリにPRがマージされると、GitHub Actionsがコンテナイメージをビルドし、ECRにプッシュします。Argo CD Image Updaterが新しいイメージを検知すると、KubernetesリポジトリにPRを自動作成します。イメージ更新PRがマージされるとArgo CDがステージング環境にデプロイし、リリースパイプラインが起動します。

リリースパイプラインでは、負荷試験、リリース用PRの作成、自動マージを行い、最終的に本番環境へデプロイします。

リリースパイプラインの全体像

この「Argo CD Sync → リリースパイプライン」のトリガー方式が、今回の改善対象です。

トリガー方式の変遷と課題

これまでトリガー方式を2度見直してきました。以降では、各方式で明らかになった課題と改善の経緯を説明します。

Phase 1: Argo CD PostSync Hook

最初は、Argo CDのPostSync Hookを使用していました。Argo CD Syncが完了すると、PostSync Hookとして定義されたKubernetes Jobが自動的に起動する仕組みです。Sync Wavesを活用してJobの実行順序を制御していました。

argo-cd.readthedocs.io

ArgoCD Sync完了 → PostSync Hook(Kubernetes Job) → 負荷試験 → リリースPR作成 → ...

課題: 不要なトリガーの発生

PostSync HookはすべてのArgo CD Sync完了時にトリガーされます。そのため、意図しないタイミングでパイプラインを起動する問題がありました。例えばConfigMapやSecretの変更時にも負荷試験が実行され、開発者の待ち時間を長くしていました。

Phase 2: Argo EventsによるRollout監視

PostSync Hookの課題を受けて、Argo Eventsを使ったRollout監視方式に移行しました。弊チームではArgo Rolloutsを利用しており、DeploymentではなくRolloutオブジェクトでPodを管理しています。RolloutはDeploymentを拡張したカスタムリソースで、カナリアリリースなどの高度なデプロイ戦略をサポートします。

この方式では、RolloutオブジェクトのステータスをKubernetes API Watchで直接監視します。イメージ更新によるロールアウト完了時のみパイプラインをトリガーする方式です。

ArgoCD Sync完了 → EventSource(Rollout監視) → Sensor → Workflow実行

また、この移行と同時にKubernetes JobからArgo Workflowsへ切り替えました。Sync Wavesによる順序制御では、各Jobの実行状況を把握しにくいという課題がありました。Argo Workflowsを採用することで、DAGによる柔軟な依存関係の定義やUIでの実行状況の可視化が可能になりました。

Sensorでは複雑なフィルタリングを行っていました。updatedReplicasreplicasavailableReplicasを比較する式フィルタと、NewReplicaSetAvailableを確認するLuaスクリプトの組み合わせです。

filters:
  data:
    - path: body.metadata.namespace
      type: string
      value: ["${service}"]
    - path: body.metadata.name
      type: string
      value: ["api-server-rollout"]
  exprs:
    - expr: updatedReplicas == replicas && updatedReplicas == availableReplicas && replicas > 0
      fields:
        - name: updatedReplicas
          path: body.status.updatedReplicas
        - name: replicas
          path: body.status.replicas
        - name: availableReplicas
          path: body.status.availableReplicas
  script: |-
    local conditions = event.body.status.conditions
    if conditions == nil then return false end
    for i, cond in ipairs(conditions) do
      if cond.type == "Progressing" and cond.reason == "NewReplicaSetAvailable" then
        return true
      end
    end
    return false

ConfigMap変更時の不要なトリガーは解消されましたが、別の課題が浮上しました。

課題: HPAスケーリングによるリリースイベントの大量発生

上記のSensorフィルタは、新しいバージョンのロールアウト完了を検知する想定で設計していました。NewReplicaSetAvailable条件とレプリカ数の一致で、新バージョンへの切り替え完了を判定しています。

しかし、HPAによるスケーリングでもRolloutオブジェクトのreplicasavailableReplicasが更新されます。スケーリング完了時にレプリカ数が一致するため、フィルタ条件を満たしてしまいます。つまり、このフィルタでは「新バージョンのロールアウト完了」と「HPAスケーリング完了」を区別できませんでした。

その結果、この問題はステージング環境で障害として顕在化しました。HPAスケーリングを起点としたイベントフラッディングにより、負荷試験が3並列で実行され、以下の問題が発生しました。

  • DB CPU使用率が100%に到達
  • api-serverがレスポンス不能に
  • CrashLoopBackOffが発生

この障害をきっかけに、トリガー方式を根本的に見直す必要があると判断しました。

Webhook EventSourceへの改善

Webhook方式の選定理由

新しいトリガー方式として、Argo CD NotificationsからWebhookでArgo Events EventSourceに通知する方式を採用しました。

ArgoCD Sync完了 → ArgoCD Notifications → Webhook → EventSource → Sensor → Workflow実行

この方式を選んだ理由は4つあります。

1. コミット単位でトリガーを制御できる

Argo CD NotificationsのoncePer: revisionにより、同一コミットSHAに対して厳密に1回だけ発火します。Rollout監視方式のようにHPAスケーリングやPod再起動でイベントが大量発生する問題は構造上発生しません。

2. トリガーソースを識別できる

Webhookペイロードにrevision(コミットSHA)が含まれるため、GitHub APIでそのコミットの変更内容を特定できます。PostSync HookやRollout監視方式ではこの情報が得られませんでした。

3. Sensorの大幅な簡素化

Rollout監視方式では、namespace、ステータス式、Luaスクリプトの3層フィルタリングが必要でした。一方、Webhook方式ではbody.appの単純な文字列マッチのみで済みます。

4. 既存基盤の活用

Slack通知で既に使用しているArgo CD Notificationsに、Webhookサービスを追加するだけで導入できます。そのため、新たなコンポーネントの導入が不要でした。

改善後の流れ

改善後の全体像は以下の通りです。

改善後のアーキテクチャ

実装の詳細

Argo CD NotificationsからWebhookを送信する

Argo CD NotificationsのConfigMapにWebhookサービスとトリガーを追加します。

# Webhookの定義
service.webhook.argo-events-sync: |
  url: http://argocd-sync-webhook-eventsource-svc.argo-events.svc.cluster.local:12000/sync
  headers:
    - name: Content-Type
      value: application/json

# テンプレート: ペイロードの定義
template.app-sync-webhook: |
  webhook:
    argo-events-sync:
      method: POST
      body: |
        {
          "app": "{{.app.metadata.name}}",
          "revision": "{{.app.status.operationState.syncResult.revision}}"
        }

# トリガー: Sync成功時、同一revisionに対して1回のみ発火
trigger.on-sync-succeeded-webhook: |
  - when: app.status.operationState != nil and app.status.operationState.phase in ['Succeeded']
    oncePer: app.status.operationState.syncResult.revision
    send: [app-sync-webhook]

oncePerがポイントです。同一のコミットSHAに対して一度しかWebhookが送信されないため、リリースパイプラインの重複実行を構造的に防止できます。

argo-cd.readthedocs.io

各環境のサブスクリプション設定で、対象のArgo CD Applicationにこのトリガーを紐付けます。

# サブスクリプション設定
defaultTriggers: |
  - recipients:
      - argo-events-sync
    triggers:
      - on-sync-succeeded-webhook

Webhook EventSourceの作成

Argo CD NotificationsからのWebhookを受信するEventSourceを作成します。

apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: argocd-sync-webhook
  namespace: argo-events
spec:
  service:
    ports:
      - port: 12000
        targetPort: 12000
  webhook:
    argocd-app-sync:
      port: "12000"
      endpoint: /sync
      method: POST

1つのEventSourceで全サービスのWebhookを受信します。サービスごとの振り分けはSensor側で行います。

Sensorの簡素化

Rollout監視方式の時代に必要だった複雑なフィルタリングが、body.appの文字列マッチのみに簡素化されました。

apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: ${service}-release-pipeline
  namespace: argo-events
spec:
  dependencies:
    - name: argocd-sync-completed
      eventSourceName: argocd-sync-webhook
      eventName: argocd-app-sync
      filters:
        data:
          - path: body.app
            type: string
            value:
              - ${service}-server-kubernetes
  triggers:
    - template:
        name: trigger-release-pipeline
        conditions: argocd-sync-completed
        k8s:
          operation: create
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: ${service}-release-pipeline-
                namespace: ${service}
              spec:
                synchronization:
                  mutexes:
                    - name: ${service}-release-pipeline
                workflowTemplateRef:
                  name: ${service}-release-pipeline
                arguments:
                  parameters:
                    - name: revision
                      value: ""
          parameters:
            - src:
                dependencyName: argocd-sync-completed
                dataKey: body.revision
              dest: spec.arguments.parameters.0.value

WebhookペイロードからコミットSHAを抽出し、Workflowのパラメータとして渡します。さらにsynchronization.mutexesを設定し、同一サービスのパイプラインが並列実行されることを防止しています。

argo-workflows.readthedocs.io

release-gate ClusterWorkflowTemplateの導入

改善前のリリースパイプラインでは、トリガーPRの特定や負荷試験の判定ロジックが各スクリプトに散在していました。これをrelease-gate ClusterWorkflowTemplateに集約し、パイプライン制御を整理しました。

release-gateの処理フロー

release-gateは4つのステップで構成されています。

Step 1: リリース差分チェック

GitHub APIでrelease...mainブランチを比較し、リリースすべき変更があるか確認します。差分がない場合はパイプラインを終了します。

Step 2: トリガーPR特定

mainブランチの最新マージコミットメッセージからPR番号を抽出します。「Merge pull request #42」のようなメッセージからPR番号を取得します。抽出に失敗した場合は、Deploymentのイメージタグ(コミットSHA)でPRを検索するフォールバックも用意しています。

Step 3: 負荷試験の判定

トリガーPRのラベルを確認します。skip_load_testラベルが付与されている場合は負荷試験をスキップし、それ以外は負荷試験を実行します。Image Updater PRは自動生成でラベルが付かないため、通常のイメージ更新では負荷試験が常に実行されます。

Step 4: auto-merge判定

リリースPR(main → release)に人間のコミットがあるか確認します。botコミット(argocd-image-updater、GitHub Actionなど)のみの場合は自動マージを有効にし、人間のコミットがある場合は無効にします。

出力パラメータ

release-gateの出力は後続のステップで条件分岐に使用されます。

パラメータ 説明
run_load_test 負荷試験の実行判定(true/false)
run_release リリースPR作成判定(true/false)
run_auto_merge auto-merge判定(true/false)
trigger_pr_number トリガーPR番号
deployment_image_tag 現在のDeploymentイメージタグ

ClusterWorkflowTemplateによるテンプレート共通化

改善前は、create-release-prauto-mergeのJobを各サービスのリポジトリにそれぞれ定義していました。4サービス分のマニフェストを個別に管理する必要があり、メンテナンスコストが高くなっていました。

ClusterWorkflowTemplateを利用することで、テンプレートをインフラリポジトリで一元管理できるようになりました。各サービスはDAGからclusterScope: trueで参照し、サービス固有の値(git-repositoryなど)はパラメータで渡します。

# 各サービスのDAGからの参照例
- name: create-release-pr
  templateRef:
    name: create-release-pr
    template: create-release-pr
    clusterScope: true
  arguments:
    parameters:
      - name: git-repository
        value: "${service}-server-kubernetes"
      - name: trigger-pr-number
        value: "{{tasks.gate.outputs.parameters.trigger_pr_number}}"

新たに追加した共通テンプレートを含め、ClusterWorkflowTemplateの全体像は以下の通りです。

ClusterWorkflowTemplate 役割
release-gate リリース判定(差分チェック、トリガーPR特定、負荷試験の要否/auto-merge判定)
create-release-pr リリースPRの自動作成
auto-merge PRの自動マージ
load-test-pr-comment 負荷試験結果をリリースPRにコメント
release-pipeline-summary パイプライン全体の結果をSlackに通知

パイプラインDAGの全体構成

最終的なパイプラインのDAG構成です。

パイプラインDAGの依存関係

spec:
  entrypoint: release-pipeline
  arguments:
    parameters:
      - name: revision
        value: ""
  templates:
    - name: release-pipeline
      dag:
        tasks:
          - name: release-gate
            templateRef:
              name: release-gate
              template: release-gate
              clusterScope: true
            arguments:
              parameters:
                - name: git-repository
                  value: "${service}-server-kubernetes"
                - name: revision
                  value: "{{workflow.parameters.revision}}"
                - name: deployment-name
                  value: "${service}-server-deployment"
                - name: deployment-namespace
                  value: "${service}"

          - name: load-test
            dependencies: [release-gate]
            when: "{{tasks.release-gate.outputs.parameters.run_load_test}} == true"
            # ...

          - name: create-release-pr
            templateRef:
              name: create-release-pr
              template: create-release-pr
              clusterScope: true
            dependencies: [load-test]
            when: "{{tasks.release-gate.outputs.parameters.run_release}} == true"
            # ...

          - name: load-test-pr-comment
            templateRef:
              name: load-test-pr-comment
              template: load-test-comment
              clusterScope: true
            dependencies: [create-release-pr, load-test]
            when: "{{tasks.release-gate.outputs.parameters.run_load_test}} == true && {{tasks.release-gate.outputs.parameters.run_release}} == true"
            # ...

          - name: auto-merge
            templateRef:
              name: auto-merge
              template: auto-merge
              clusterScope: true
            dependencies: [create-release-pr, load-test-pr-comment]
            when: "{{tasks.release-gate.outputs.parameters.run_auto_merge}} == true"
            # ...

          - name: release-pipeline-summary
            templateRef:
              name: release-pipeline-summary
              template: summary
              clusterScope: true
            dependencies: [release-gate, load-test, create-release-pr, auto-merge]
            when: "{{tasks.release-gate.outputs.parameters.run_release}} == true"
            # ...

dependencieswhenを組み合わせることで、各ステップの実行条件を柔軟に制御しています。dependenciesはタスクの依存関係(実行順序)を定義します。一方、whenはrelease-gateの出力パラメータに基づいてタスクの実行可否を判定します。

例えばcreate-release-prload-testに依存しつつ、run_release == trueの場合にのみ実行されます。負荷試験がスキップ(Omitted)された場合も依存関係は満たされるため、後続のタスクは実行されます。

スクリプトの簡素化

release-gateにロジックを集約したことで、create-release-prauto-mergeのスクリプトを大幅に簡素化できました。

削除した機能 移動先
トリガーPR特定ロジック release-gate
人間コミットチェック release-gate
Slack通知 release-pipeline-summary

両スクリプトはTRIGGER_PR_NUMBER環境変数をrelease-gateから受け取るだけのシンプルな実装になりました。

導入効果

パイプラインの可視化

以前は、PostSync HookとKubernetes Jobを使用していたため、パイプラインの進行状況を把握しにくい状態でした。Argo Workflowsに移行したことで、DAGの実行状況をArgo Workflows UIで視覚的に確認できるようになりました。

さらに、release-pipeline-summaryによるSlack通知でパイプライン全体の実行結果を一目で把握できます。負荷試験結果はリリースPRにもコメントされるため、手動マージ時の判断も容易です。

不要なトリガーとイベントフラッディングの解消

Phase 1の課題であった不要なトリガーについては、release-gateのリリース差分チェックで解消しました。Webhook方式ではすべてのSync成功時にパイプラインが起動しますが、release-gateが差分を判定し、リリースすべき変更がなければ早期終了します。

Phase 2の課題であったイベントフラッディングについては、oncePer: revisionにより解消しました。HPAスケーリングやPod再起動に起因するイベントの大量発生を防げるようになりました。

Sensorの大幅な簡素化

3層フィルタリング(namespace + ステータス式 + Luaスクリプト)から、body.appの文字列マッチのみに簡素化されました。これにより、Sensorの定義が大幅にシンプルになり、メンテナンス性が向上しました。

マルチサービスへの横展開の容易さ

ClusterWorkflowTemplateとして共通ロジックを一元管理しているため、新しいサービスへの展開が容易です。Sensorの追加とDAGの定義、負荷試験用のWorkflowTemplateの作成だけで完了します。

まとめ

リリースパイプラインのトリガー方式は、PostSync Hook → Rollout監視 → Webhook EventSourceと変遷してきました。今回Argo WorkflowsとArgo Eventsを活用し、Webhook EventSourceへの移行を実現しています。

各方式の課題を段階的に解消できたのは、Argo Eventsの柔軟なイベントソースのおかげです。特に、Argo CD NotificationsのoncePer機能とWebhook EventSourceの組み合わせは、イベント駆動型パイプラインの制御に有効でした。

また、今回の改善を通じて、複数サービスで共通するパイプラインの変更を安全に進める方法を見直すきっかけにもなりました。改善の過程でパイプラインが検証通りに動作せず、リリースが停止するトラブルも発生しました。リリースパイプラインの変更は4サービスに同時に影響するため、慎重なアプローチが求められます。今後の改善においては、変更によるリスクを最小化する方法も検討していきたいと考えています。

本記事がArgo EventsやArgo Workflowsを活用したリリースパイプラインの構築を検討している方の参考になれば幸いです。

おわりに

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com




以上の内容はhttps://techblog.zozo.com/entry/release-pipeline-argo-events-workflowsより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14