Argo CD導入設計とリリースフロー改善の取り組み

ogp

はじめに

こんにちは、計測プラットフォーム開発本部SREブロックの渡辺です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。

先日私達のチームでは、EKS環境にArgo CDを導入し、デプロイパイプラインのリアーキテクトを行いました。

開発環境では、Argo CD Image Updater(以下、Image Updaterとする)を活用したスピーディなデプロイ設計をしました。詳しくは「EKS環境へArgo CD Image Updaterを導入し、デプロイ時間と管理コストを削減した話」を参照ください。

techblog.zozo.com

本記事では、Argo CD導入による本番環境のリリースフロー設計やタグ更新の仕組みなど工夫した点について紹介します。Argo CDを検討している方に向けて、少しでも参考になれば幸いです。

目次

Argo CD導入前の課題

ここでは例としてZOZOMATにおけるArgo CD導入前のCI/CDアーキテクチャを下図に示します。

既存のCI/CDアーキテクチャ

Argo CD導入前までは、以下の手順でリリースしていました。

  • CI
    1. アプリケーションの変更をmainブランチへ取り込む
    2. CircleCIのイメージビルド用のジョブが発火する
    3. skaffold buildを実行し、イメージをビルドしてECRにプッシュする
    4. ビルド時に生成されたイメージタグが記録されたjsonファイルをS3にアップロードする
  • CD
    1. ビルド完了を待ち、CDをトリガーするスクリプトを実行する(引数でデプロイ先の環境を指定)
    2. CircleCIからCodePipelineのアクションプロバイダーであるS3にソースコードをアップロードする
    3. CodeBuildがソースとイメージタグファイルをS3から取得し、Skaffoldを使ってapplyする

旧CI/CDの問題点については、先の記事でも説明していますが、本番環境も同様に大きな課題はCD部分にありました。

開発環境ではスピーディなデプロイが求められましたが、本番環境では以下のようなデプロイ作業の不安定さがより課題感としてありました。

  • CIの完了確認やデプロイする環境はリリース担当者が判断する
  • CDのトリガーをリリース担当者が手作業で行う
  • 各プロダクトごと作業内容が異なる(ZOZOMATとZOZOGLASSなど)
  • 問題発生時の調査範囲が広くなり、ロールバック判断が遅れる
  • 横展開の工数が増える(0->1でCI/CDを構築する必要がある)
  • 新規にジョインしたメンバーのキャッチアップコストが大きい

Argo CD導入

Argo

上記に挙げたCDの課題を解決するため、Argo CDを導入することにしました。

Argo CDは、Kubernetes環境でのGitOpsを実現するためのCDツールです。Gitリポジトリで管理しているKubernetesマニフェストを監視して、Kubernetesクラスタに適用します。Gitを信頼できる唯一のソースとすることで、Kubernetesクラスタ内の状態を管理するものです。

GitOpsを実現するツールとして、Argo CDの他にもFluxやJenkins Xなどが挙げられます。この中でArgo CDを採用した大きな理由は、「Syncの状況が把握しやすい」「GUIの操作で特定のリソースだけをSyncできる」などGUIが優れている点になります。また、リリースはSREだけでなくバックエンドチームも担当するため、わかりやすいツールで運用することは上で重要なポイントだと判断しました。

それでは、私達がArgo CDを導入するにあたり検討した以下の内容について説明します。

  • Argo CDのProject設計
  • GitHubリポジトリ設計
  • ブランチ戦略
  • CI/CD設計
  • イメージタグの更新方法

Argo CDのProject設計

Argo CDにはProjectという概念があります。ProjectはApplicationをグループ化するものです(ApplicationはGitOpsするGitリポジトリの設定)。

Projectには主に以下の機能があります。詳しくは公式ドキュメントを参照してください。

  1. デプロイできるソース (GitHub) を制限する機能
  2. デプロイする対象 (Clusterやnamespace) を制限する機能
  3. デプロイできるObject(Kubernetesリソース)を制限する機能
  4. 操作権限を制限するRBACを提供する機能

今回、私達が厳密に制御したかったのはデプロイ対象のnamespaceでした。

これは、私達が単一のEKSクラスタの上で複数のサービスを運用するシングルクラスタ・マルチテナント構成を採用していることが大きな理由になります。シングルクラスタ・マルチテナント運用については、「EKSのマルチテナント化を踏まえたZOZOGLASSのシステム設計」を参照してください。

techblog.zozo.com

クラスタ設計

1つのクラスタにプロダクト毎namespaceを作成しているため、あるプロダクトのリソースが別のプロダクトのnamespaceにデプロイできない制限を設ける必要があるのです。

そこで、上記のうち2番目の「デプロイする対象 (Clusterやnamespace) を制限する機能」に関して、Projectの機能を利用したnamespaceの制限について紹介します。

Argo CDは初期設定としてdefaultという名前のProjectが作成されてますが、こちらは制限なしのProjectです。全てのApplicationをdefault Projectで管理する問題として、どのApplicationからでも複数のnamespaceにデプロイできてしまうことが挙げられます。

例えば、ZOZOMATをzozomat namespace、ZOZOGLASSをzozoglass namespaceで管理している場合、ZOZOMATのApplicationからzozoglass namespaceにデプロイできる状態になってしまいます。

そこで私達は、これを防止すべく下図のように1namespace 1Projectで設計しました。

Argo CD Project設計

設計当初は1Application 1Projectでよりセキュアにする考えもあったのですが、この設計ではApplicationのグループ化を行えないデメリットがありました。

今後namespace内でマイクロサービス化する可能性があり、1つのnamespaceに複数のApplicationを設定することが予想されます。このため、1namespace 1Projectで管理することが望ましいと考えました。

ただ、今回はArgo CD導入初期として1namespace 1Projectで設計しましたが、今後プロダクトで共通利用するnamespaceを作成する可能性もあります。このため、変化する状況に合わせて柔軟に対応していきたいと思います。

GitHubリポジトリ設計

計測システム部では、バックエンドとSREは別のチームとして存在しています。以前はプロダクトごとにアプリケーションリポジトリ(以下、アプリリポジトリ)の中でKubernetesマニフェストを管理していました。

私達が1つのリポジトリで管理して実感した課題は以下のとおりです。

  • アプリケーションの変更を追跡しづらい
    • コミット履歴にKubernetesマニフェストの変更がノイズとして入る
  • ロールバックする際、アプリケーションは戻したいけどKubernetesマニフェストは戻したくない場合に面倒
  • Kubernetesマニフェストの変更でもCI(イメージビルド)が動く
    • podの数を変えたいだけなのにアプリケーションのテストが走るストレス
  • リポジトリ運用をインフラとバックエンド間で調整する必要がある
    • ブランチ戦略を決める際に双方の要望を叶えようとするとリポジトリ設定やCI定義等が複雑化しがち
  • アプリリポジトリに本番環境を変更する仕組みや権限を配置する必要がある

Argo CDのベストプラクティスでは、アプリケーションのソースコードとKubernetesマニフェストを分けて管理することが推奨されています。このため、プロダクトごと新たにKubernetesマニフェストを管理するリポジトリ(以下、Kubernetesリポジトリ)を作成しました。

チーム単位でリポジトリを分けたことで、上記のデメリットを解消できました。

ブランチ戦略

ここではKubernetesリポジトリのブランチ戦略を紹介します。

Argo CDは、Applicationの設定においてターゲットとしてリポジトリのブランチを指定できるため、環境ごとにブランチを分けることで1つのリポジトリで複数環境を構築できます。

そこで、私達はmainブランチをステージング環境、releaseブランチを本番環境として運用しています。デフォルトブランチをmainに設定し、PRの向き先をmainブランチへ指定します。

本番環境のターゲットとなるreleaseブランチはmainブランチを追従し、ステージング環境で動作確認してから本番環境に反映します。

余談ですが、アプリリポジトリと分けたことで、ブランチ戦略がバックエンドチームと競合しなくなりました。ブランチ戦略の決定権がSREチーム内にあるため、他チームとの調整が不要になり設定が楽になりました。

CI/CD設計

それでは、本記事のメインとなるArgo CD導入後の本番リリースフローについて説明します。

Argo CDによるCI/CDアーキテクチャ

開発環境との大きな違いは、承認フローを設けているところです。

先の記事で説明していますが、開発環境では作業効率を優先したためImage Updaterを活用した承認フローなしのデプロイを行っています。

しかし、本番環境ではデプロイ時間の短さよりも安定性を求めているため、承認フローは必須条件です。このため開発環境とは別の仕組みでリリースフローを検討する必要がありました。

通常、アプリケーションをデプロイするためには、アプリリポジトリでビルドしたイメージをKubernetesマニフェストに反映させる必要があります。

ここでは、Push型の方法としてアプリリポジトリのCIでKubernetesリポジトリを取り込み新しいタグに書き換え、変更コミットを含むブランチを作成する仕組みを構築することが一般的かと思います。

私達も当初はそのような仕組みを考えていました。しかし、開発環境で導入したImage Updaterにイメージリポジトリの変更を検知して、イメージを変更するコミットを含むブランチを自動作成する機能があることを知りました。こちらはPull型の方法で、ECRなどをImage Updaterが監視して最新のイメージを検知した際、Push型同様Kubernetesリポジトリに変更コミットを含むブランチを作成するものです。

今回は、検証の意味も含めImage Updaterを活用した仕組みを構築しました。イメージタグ更新の仕組み下図に示します。

イメージタグ更新の仕組み

  1. アプリリポジトリでビルドしたイメージがECRにプッシュされる
  2. Image Updaterが最新のイメージを検知
  3. Image UpdaterからKubernetesリポジトリにタグ更新コミットつきブランチを作成
  4. GitHub Actionsでブランチ作成をトリガーにmainブランチ向けリリースPRを作成

アプリリポジトリのCIはイメージをECRにプッシュするまでを責務とし、イメージを検知したImage UpdaterがKubernetesリポジトリに変更を通知するような構成になっています。Kubernetesリポジトリ側は、通知をトリガーにGitHub Actionsでタグを更新するPRを作成するというのが大まかな流れになります。

イメージタグの更新方法

それでは、上記の仕組みの構築方法について説明していきます。

まずは事前準備として、ステージング環境にImage Updaterをインストールし、Argo CDに設定しているApplicationのannotationsに以下の設定を追記します。

metadata:
  annotations:
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: main:image-updater-{{.SHA256}}
    argocd-image-updater.argoproj.io/image-list: my-image=<AWS Account>.dkr.ecr.<Region>.amazonaws.com/<ECR Repo>
    argocd-image-updater.argoproj.io/my-image.update-strategy: latest
    argocd-image-updater.argoproj.io/my-image.ignore-tags: latest

こちらの設定により、イメージを検知したImage Updaterが変更コミットつきブランチを作成します(この他にも柔軟な設定ができるので、詳しくは公式ドキュメントを参照してください)。

なお、Image UpdaterはECRのイメージ情報を取得する権限が必要になります。ここでは詳しく説明しませんが、気になる方は先の記事を参照してください。

それでは、Image UpdaterがKubernetesリポジトリにどのようなアクションを行うのか見ていきましょう。

私達のチームでは環境差分を管理しやすくするためkustomizeを用いており、overlaysディレクトリの中で各環境のディレクトリを作成しマニフェストを管理しています。

最新のイメージを検知したImage Updaterがoverlays/stagingディレクトリにあるイメージ管理ファイルのタグを変更するコミットつきブランチを作成します。

イメージ管理ファイルは、.argocd-source-<Application名>.yamlという名前で以下のような内容になります。

kustomize:
  images:
  - <AWS Account>.dkr.ecr.<Region>.amazonaws.com/<ECR Repo>:<Tag>

この時点では、まだmainブランチのファイルを更新していないため、ステージング環境のPodが入れ替わることはありません。

次にGitHub ActionsでImage Updaterが作成したブランチをトリガーに、mainブランチ向けステージングリリースPRを作成します。

先ほど紹介したArgo CDの各Applicationのannotationsのうち、以下の設定によりimage-updaterから始まるブランチが作成されます。

argocd-image-updater.argoproj.io/git-branch: main:image-updater-{{.SHA256}}

そして、GitHub Actionsのトリガーを以下のように設定することで、Image Updaterが作成したブランチのみをトリガーにアクションが実行されるようにします。

on:
  push:
    branches:
      - 'image-updater-**'

GitHub Actionsでは、「overlays/productionディレクトリにある本番環境用イメージ管理ファイルをステージング同様の変更に更新する処理」や「PRを作成する処理」を実行します。なお、1つ目の処理をmainブランチに反映しても本番環境に反映されることはありません(ターゲットとなるreleaseブランチには変更が反映されていないため)。

mainブランチ反映後、ステージング環境のArgo CDが差分を検知しデプロイします。ステージング環境の動作確認に問題がなければ、GitHub Actionsで自動作成されたreleaseブランチ向け本番リリースPRをマージします。releaseブランチ反映後、本番環境のArgo CDが差分を検知しデプロイします。

ちなみに、本番環境にもImage Updaterをインストールしてステージング環境と同様の仕組みを構築できますが、以下の懸念点があるため私達は本番環境への導入は見送りました。

  • mainとreleaseブランチを独立して運用することで、ステージング環境で動作確認したイメージと同じものをデプロイする保証がなくなる

私達のチームではステージング環境と本番環境の差分をなるべく減らして運用しています。安定したリリースフローを構築するためには、ステージング環境で動作確認したバージョンを確実にリリースする必要があると考えています。このため、「releaseブランチはmainブランチを追従する」というブランチ戦略を崩す選択はできないのです。

なお、ステージング環境で動作確認したイメージが本番環境のECRに存在することを保証するため、GitHub Actionsでイメージタグを検証する処理を追加しています。このアクションは本番リリースPRが作成されたタイミングで発動し、失敗するとPRをマージできない仕組みになっています。

Image Updaterの考慮すべき点と対応

Image UpdaterとGitHub Actionsを組み合わせることで自動リリースPRが作成されますが、運用してみると工夫しなくてはいけない問題が出てきました。

当初、私達は下図のように自前管理している全てのイメージをImage Updaterの監視対象としていましたが、以下のような不都合が生じました。

  • Image Updaterの監視サイクルによって変更を検知しないイメージが出る場合もある
  • 同じイメージにタグがついた場合、Image Updaterが検知しない

Image Updaterの監視対象を全イメージ

まず、「Image Updaterの監視サイクルによって変更を検知しないイメージが出る場合もある」から説明します。

私達が管理しているCIでは各イメージを並列でビルドして、完了したイメージからECRにプッシュしています。

このため、Image UpdaterがECRの情報を取得するタイミングによっては、プッシュが完了したイメージもあればプッシュが完了していないイメージもあります。

Image Updaterは1つでもイメージを検知すればブランチを作成するため、自動作成されたPRを確認すると一部のイメージが更新されていないケースもありました。

時間の経過と共に全てのイメージのプッシュが完了するため、時間を空けると全てのイメージを更新するPRが作成されますが、複数のPRが存在することでオペレーションミスが発生することも考えられます。

一部イメージタグが更新されないPR

次に「同じイメージにタグがついた場合、Image Updaterが検知しない」を説明します。

私達はCircleCIでSkaffoldを利用してDockerイメージをビルドしECRへプッシュしており、高速化のためブランチごとキャッシュを持っています。

自前管理しているイメージのうち一部は内容に変更がない場合もあり、ECRへプッシュする際にタグが既存のイメージに付与されることがあります。

Image Updaterには最新のイメージを検知するよう設定しているため、既存のイメージにタグ付けされた場合は検知されません。このケースは時間が解決する問題ではないため、イメージタグがバラバラになってしまいます(同じイメージを参照しているが保守性が失われる)。

1つのイメージに複数タグがつく

そこで、上記2つの課題を解決するため、下図のようにアプリケーション本体のイメージだけを監視し、GitHub Actionsで他のイメージに同様のタグをつける処理を実装しました。

Image Updaterの監視対象を一部イメージ

アプリケーションのイメージは最もビルドに時間がかかり、リリースのたびに内容が変更されるため新しいイメージとしてECRにプッシュされるため、単一の監視対象として適切でした。

自動作成されたPRを確認すると、最初のコミットはImage Updaterが検知したアプリケーション本体のイメージタグの更新です。

1つ目のコミット

次のコミットは、GitHub Actionsで処理している他のイメージタグをアプリケーション本体のイメージタグと統一させる変更です。

2つ目のコミット

最後のコミットは、GitHub Actionsで処理している本番環境のタグ管理ファイルをステージングのタグ管理ファイルと同じ内容に書き換える変更です。

3つ目のコミット

なお、GitHub Actionsの処理は各プロダクトで共通しているので、差分をパラメータ化した共通アクションとすることで管理コストを抑えることができました。

GitHub Actions共通化

共通化したアクションはこのような内容になります。

inputs:
  argocd-application:
    description: Argo CD Application Name
    required: true
  source-image:
    description: image watched by Image Updater
    required: true
  target-images-to-duplicate-image-tag:
    description: images apart from source-image. The format of item is `imageA,imageB,imageC`
    required: true

runs:
  using: composite
  steps:
    - name: Update production file
      shell: bash
      run: |
        git config --global user.email "action@github.com"
        git config --global user.name "GitHub Action"

        STG_FILE="kubernetes/overlays/staging/${{ inputs.argocd-application }}.yaml"
        PRD_FILE="kubernetes/overlays/production/${{ inputs.argocd-application }}.yaml"

        # source-imageのタグを全イメージに反映する。
        IMAGES=${{ inputs.target-images-to-duplicate-image-tag }}
        for image in ${IMAGES//,/ };
        do
          grep -oE ".+${{ inputs.source-image }}:[0-9a-z\-]+$" $STG_FILE | sed "s/${{ inputs.source-image }}/$image/g" >> $STG_FILE
        done

        # ステージング用のイメージタグの変更を本番用のファイルにも反映させる
        sed -e 's/<ステージング環境のAWS Account>/<本番環境のAWS Account>/g' $STG_FILE > $PRD_FILE

        git add $STG_FILE
        git commit -m 'update image tags on staging'
        git add $PRD_FILE
        git commit -m 'update image tags on production'
        git push origin HEAD
    - name: Create PR to main branch
      uses: actions/github-script@v6
      with:
        script: |
          const { repo, owner } = context.repo;
          const result = await github.rest.pulls.create({
            title: 'イメージタグの更新',
            owner,
            repo,
            head: '${{ github.ref_name }}',
            base: 'main',
            body: 'staging 環境と production 環境のイメージタグを更新する。\n - staging リリース: main ブランチにマージすると自動で staging 環境の Pod が入れ替わる。\n - production リリース: main から release ブランチの PR をマージすると自動で production 環境の Pod が入れ替わる。'
          });

呼び出し側のアクションは以下のようになります。

on:
  push:
    branches:
      - 'image-updater-**'

jobs:
  create-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Checkout common actions repository
        uses: actions/checkout@v3
        with:
          repository: <共通化リポジトリ>
          path: common
          token: <Personal Access Token>
      - name: Use the action in common actions repository
        uses: ./common/.github/actions/<共通アクション>
        with:
          argocd-application: <タグ管理ファイル名>
          source-image: <Image Updaterが監視するアプリケーションイメージ>
          target-images-to-duplicate-image-tag: <source-image以外のイメージ群> # The format of item is `imageA,imageB,imageC`

ロールバック

ロールバックについて、以前はスクリプトを実行してリリース前のタグを指定して再度デプロイしていました。

今回、デプロイのトリガーをPRで管理するようにしたため、本番リリースPRをリバートするだけでロールバック作業が完了します。

ロールバックが必要な状況下においては、素早くかつ正確な作業が求められるため、PRをリバートするだけの仕組みはとても理想的な形になりました。

また、アプリケーションとKubernetesの変更が1つのPRに混在するケースは稀なので、リバートするべきPRを選別する手間はほとんど発生しません。

なお、現在は一部プロダクトでArgo Rolloutsを導入し、デプロイ作業中に異常を検知したら自動ロールバックするといった仕組みを構築しました。

導入前後の比較

リアーキテクト前 リアーキテクト後
デプロイ時間 約8分 3分以内
CDに利用するツール CodePipeline, CodeBuild, Shell Script, Skaffold Argo CD, Argo CD Image Updater
オペレーションミスの可能性 別環境へデプロイする可能性あり 別環境へデプロイする可能性なし
横展開のしやすさ ×

Argo CDを導入したことで様々な恩恵を受けましたが、中でも横展開しやすさが私達のプロダクトと非常にマッチしていると実感しました。

図6 シングルクラスタ・マルチテナント

先ほど紹介したとおり、私達はシングルクラスタ・マルチテナント構成で運用しているため、一度Argo CDを導入すればApplicationリソースにプロダクトを追加するだけでCDの構築が完了します。私達の計測サービスは新規事業に関わる事が多い部署であり、0→1のサービス開発が多いため、ビジネススピードに対応できることは大きなメリットです。

また、各プロダクトごと異なっていたリリース手順を共通化できたことは、管理コストを抑えることに繋がります。複数のプロダクトを管理する状況下においては、マルチテナントとArgo CDの相性の良さを実感しました。

まとめ

今回、Argo CDを導入するにあたり、GitHubリポジトリやブランチ戦略、Argo CDのPropject設計、リリースタグ更新の仕組みなど、様々な設計を検討する必要がありました。試行錯誤しながら私達のチーム状況に合わせたCDリアーキテクトが実現できたと思います。

今回紹介したデプロイフローの中で特徴的なのが、Image UpdaterとGitHub Actionsを組み合わせた自動PR作成の仕組みになります。

実際に運用して感じたメリットは、仕組みを横展開しやすいことです。マルチテナントで複数のプロダクトを運用している私達は、Image Updaterで全プロダクトのイメージを監視し、リリースPRを共通アクションで作成することは大きなメリットだと実感しています。

一方でデメリットは、複数のイメージを管理する場合、Image Updaterの動作に合わせて処理をカスタマイズする必要があることです。このため、複数プロダクトを運用しない環境においては、アプリリポジトリのCIでタグを更新する仕組みを構築した方が管理しやすいかもしれません。

Image Updaterを活用するメリット・デメリットを考慮して、プロダクトに合ったタグ更新の仕組みを構築するといいと思います。

終わりに

計測プラットフォーム開発本部では、今後もZOZOFIT等の新しいサービスのローンチを予定しています。更にスピード感を持った開発が求められますが、このような課題に対して楽しんで取り組み、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。

hrmos.co

カテゴリー