
はじめに
こんにちは。グローバルプロダクト開発本部SREブロックの纐纈です。
弊チームでは、Kubernetes上で動作する4つのサービス(ZOZOMAT、ZOZOGLASS、ZOZOMETRY、お試しメイク)のリリースを自動化しています。これまでにArgo CDによるGitOpsやArgo Rolloutsによるカナリアリリースを導入してきました。
リリースパイプラインの全体像については以下の記事で紹介しています。
本記事では、このリリースパイプラインのトリガー方式を見直した取り組みについて紹介します。改善にあたり、Argo EventsとArgo Workflowsを活用しました。Argo Eventsはイベント駆動型の自動化フレームワークで、EventSourceで様々なイベントを受信しSensorで後続処理をトリガーできます。Argo WorkflowsはKubernetes上でDAG形式のワークフローを実行するエンジンです。
目次
リリースパイプラインの全体像
まず現在運用しているリリースパイプラインの全体像を説明します。
弊チームでは、アプリケーションコードを管理するサーバーリポジトリと、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の実行順序を制御していました。
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では複雑なフィルタリングを行っていました。updatedReplicas、replicas、availableReplicasを比較する式フィルタと、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オブジェクトのreplicasやavailableReplicasが更新されます。スケーリング完了時にレプリカ数が一致するため、フィルタ条件を満たしてしまいます。つまり、このフィルタでは「新バージョンのロールアウト完了」と「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 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を設定し、同一サービスのパイプラインが並列実行されることを防止しています。
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-prやauto-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構成です。

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" # ...
dependenciesとwhenを組み合わせることで、各ステップの実行条件を柔軟に制御しています。dependenciesはタスクの依存関係(実行順序)を定義します。一方、whenはrelease-gateの出力パラメータに基づいてタスクの実行可否を判定します。
例えばcreate-release-prはload-testに依存しつつ、run_release == trueの場合にのみ実行されます。負荷試験がスキップ(Omitted)された場合も依存関係は満たされるため、後続のタスクは実行されます。
スクリプトの簡素化
release-gateにロジックを集約したことで、create-release-prとauto-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では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。