GKEにArgoCD+Image Updaterを導入してリリース作業時間を大幅改善した話!!!

はじめに

はじめまして!!!MIRAIZエンジニアリンググループ共通基盤チームの増田です。2023年卒として新卒入社し、昨年の8月からMIRAIZに参画しています。 共通基盤チームでは、非機能要件の強化・改善や障害対応などを行っています。技術的には、Google CloudやTerraform、Datadogに触れる機会が多いですが、アプリケーション側の修正を行うこともあり、幅広く対応しています。

本記事では、MIRAIZで導入したArgoCDについて触れます。ただ、実装の詳細まで触れると1つの記事に収まらないので、一部の文章や構成図が抽象的な内容になってしまうことをご了承ください。

要約

  • リリース作業時間が長時間化する課題を抱えていました。改善するために、GitOpsとArgoCDに着目しました。
  • ArgoCDの導入は試行錯誤の連続でした。そこで、実装のハマりやすいポイントを本記事でまとめました。
  • ArgoCDの導入によって、リリース作業時間が最大2時間から10分に改善しました。

導入背景

MIRAIZは、数十個のサービスが動作するマイクロサービス構成を採用しており、Google Kubernetes Engine(GKE)上で運用されています。 各サービス毎にリポジトリが分かれており、アプリケーションコードとマニフェストが同一のリポジトリで管理されています。各環境ごとの差分も、この同一リポジトリ内でKustomizeで対応しています。デプロイは、ソースコードが管理されているリポジトリのGitHub ActionsのワークフローからGKEに対して、Skaffoldを使って行われます。これは、Push型のデプロイと言えます。

しかし、この構成は2つの課題を抱えていました。

① リリース作業の長時間化

GKEの限定公開クラスタで接続可能IPを制限しているため、デプロイフロー毎に接続可能IPの設定を行う必要があります。この設定変更を並列で実行するとエラーが発生します。また、CI/CDが分離されていないため、リリース当日にイメージのビルド・プッシュを行う必要があり、接続可能IPの設定変更が重ならないように順列にデプロイフローを実行させると、リリース作業時間が長時間化してしまいます。

1サービス10分のデプロイ時間 × リリース対象数 → 最大で2時間

② アプリケーションコードとマニフェストの同一リポジトリ管理

CI/CDが分離されていないため、アプリケーションコードとマニフェストを同一リポジトリで管理する必要があります。しかし、これによりアプリケーションの変更とマニフェストの変更が重なり、追跡しづらくなります。さらに、各リポジトリでマニフェストが分散管理されているため、Horizontal Pod Autoscaler(HPA)の設定など、DeploymentやServiceのマニフェストに対して、変更を加える際は各リポジトリで対応しなければなりません。

課題

GitOpsとArgoCD

前述した2つの課題の解決に向けて、MIRAIZの共通基盤チームではGitOpsとArgoCDに着目しました。

GitOps

GitOpsとは、マニフェストの管理においてGitをSingle Source Of Truth(SSOT)とする考え方です。つまり、Gitの状態=クラスタの状態を表します。Gitに反映された変更は、GitOpsツールによって開発環境や運用環境に自動的に反映されます。これは、Pull型のデプロイと言えます。

GitOps概要

GitOpsなCDを実現するツールとして、ArgoCDとFluxCDを比較しました。 どちらのプロジェクトも、Cloud Native Computing Foundation(CNCF)によってGraduatedステータスを取得しており、ツールとしての成熟度は高いです。今回は、管理画面が用意されていることから、ArgoCDを採用しました。

ツール名 CNCF成熟度 GitHub Star 管理画面
ArgoCD Guraduated 16.8k あり
FluxCD Guraduated 6.2k なし

ArgoCD

ArgoCDは、GitOpsによるCDを実現するツールです。前述しましたが、CNCFのGraduatedプロジェクトであり、ツールとしての成熟度も高いです。同じGraduatedプロジェクトにはKubernetesやHelmなどがあります。リッチなUIを持つ管理画面が特徴で、クラスタに導入することで利用可能です。 GitOpsに対応しているため、マニフェストを修正するだけでクラスタに新たな状態を反映できます。修正を反映する際には、Pull Request(PR)を作成し、ArgoCD側で指定したブランチへのマージがリリースを意味します。

ArgoCDを導入することで、MIRAIZで抱えていた2つの課題は解決できます。

① リリース作業の長時間化

ArgoCDはクラスタ内のリソースとして動作するため、デプロイ時にIP制限の設定変更が不要になり、デプロイ処理の競合を回避できるので、並列デプロイが可能になります。これまでのCI/CDのワークフローから、CIのみの役割となったので、予めイメージのビルドとプッシュしておくことで、事前にリリース準備作業が行えます。当日はPRをマージするだけでイメージが反映され、リリースが完了します。

② アプリケーションコードとマニフェストの同一リポジトリ管理

アプリケーションコードとマニフェストを分離し、マニフェストはアプリケーションコードとは別の構成管理用のリポジトリで集約管理します。 この管理方法を分けたことにより、アプリケーションとマニフェストの変更が個別に追跡可能になります。ArgoCDでは、アプリケーションコードとマニフェストを別々のリポジトリで管理することが推奨されていることもあり、ArgoCDでの運用上においても望ましい構成に移行することができました。

ArgoCDを導入した場合の構成

ArgoCD + Image Updaterの構成に

ArgoCDを導入することで、2つの課題は解決できます。 ただ、ArgoCDを開発環境・ステージング環境・本番環境に導入するということは、3環境分の変更がマニフェストを集約した1つのリポジトリにPRとして入ることになります。PRをマージしなければ新たなイメージタグの変更を反映させることができないため、開発環境・ステージング環境で動作検証する際のスピード阻害の要因となります。つまり、本番環境は完全なGitOpsにしたいが、開発環境・ステージング環境は効率的にデプロイしたいのです。

そこで、ArgoCDに加え、拡張ツールであるImage Updaterを導入しました。

Image Updaterは、ArgoCD同様にArgoプロジェクトの一部として開発されており、Artifact Registryにプッシュされるイメージを検知し、ArgoCDと連携してイメージの変更を行うツールです。2つのイメージタグ変更パターン(write-back-method)が用意されています。環境ごとに変更パターンを使い分けることで、効率的なデプロイを実現可能です。

write-back-method: git

最新のイメージを検知し、イメージタグの変更をしたブランチをリポジトリに作成するパターンです。作成されたブランチをマージすることで、変更されたイメージタグの内容はデプロイされます。MIRAIZの場合、本番環境で使いたいパターンです。

Image Updater(write-back-method: git)を導入した場合の構成

write-back-method: argocd

最新のイメージを検知し、内部的にイメージタグを変更するパターンです。マニフェスト上でイメージタグは管理しないため、リソースをクラスタから削除して再作成すると、Image Updaterによって行われた変更は失われます。MIRAIZの場合、開発環境とステージング環境で使いたいパターンです。

Image Updater(write-back-method: argocd)を導入した場合の構成

実装概要

ArgoCD + Image Updaterの構成の重要なポイントを挙げます。

導入

ArgoCD、Image Updater共にHelmでクラスタに導入しました。

ArgoCD管理画面

ArgoCDの管理画面は、Ingressリソースにargocd-serverを紐づけて、アクセス可能にしています。追加でGoogle CloudのIdentity-Aware Proxy(IAP)も導入することで、管理画面自体を特定ユーザーのみ閲覧可能にしています。

Ingressを使ってargocd-serverに紐づける

↓ (参考)Ingressに紐づけるService例

apiVersion: v1
kind: Service
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    cloud.google.com/neg: '{"ingress": true}'
    # backendconfigでIAPの設定をする
    cloud.google.com/backend-config: '{"ports": {"http":"argocd-backend-config"}}'
  labels:
    app.kubernetes.io/component: server
    app.kubernetes.io/name: argocd-server
    app.kubernetes.io/part-of: argocd
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  - name: https
    port: 443
    protocol: TCP
    targetPort: 8080
  selector:
    app.kubernetes.io/name: argocd-server

Applicationリソース

ArgoCDでマニフェストを管理するには、Applicationというリソース(以降、Applicationリソースと呼ぶ)を適用する必要があります。

  • sorce : 参照するリポジトリの設定を示します。Kustomizeを使っているので、git@github.com:*****/*****.gitリポジトリのk8s/api/sample-api/overlays/devにあるkustomization.yamlの適用内容をArgoCDの管理下にします。ArgoCDでは、Kustomize以外にもHelmに対応しているようです。

  • syncPolicy : 手動同期または自動同期の設定を示します。全環境で自動同期を採用しています。本番環境は手動同期にするべきかもしれませんが、自動同期で運用している中で問題は発生していません。

↓ (参考)Applicationリソース例

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sample-api
  namespace: argocd
spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    path: k8s/api/sample-api/overlays/dev
    repoURL: git@github.com:*****/*****.git
    targetRevision: main
  syncPolicy:
    automated:
      prune: false
      selfHeal: true

Image Updater

Image Updaterを利用するには、Applicationsリソースにannotationsを追加する必要があります。いくつか指定していますが、重要な部分だけ説明します。

  • write-back-method : 前述の通り、デプロイ方法を示します。

  • image-list : コンテナレジストリのパスを示します。

  • pull-secret : Artifact Registryへの認証に使うキーを示します。MIRAIZでは、External Secret Operator(ESO)を使用して、Google CloudのSecret Managerと同期を実現しています。そのため、Artifact Registryにアクセス可能なサービスアカウントキーを発行し、Secret Managerに保管しています。このサービスアカウントキーをESOを使ってクラスタ内で利用します。本来であれば、Workload Identityによる認証が望ましいですが、実現できなかったため、サービスアカウントキーによる認証を採用しています。

  • update-strategy : Artifact Registryのコンテナイメージを検知する際の方法を示します。digestlatestなどがあります。開発環境・ステージング環境と本番環境でupdate-strategyが異なる話は後述します。
    ※ Image Updaterがのバージョンが0.13に上がる際に一部のupdate-strategyの名称が変更されています。本記事では、変更以前のものに統一しています。

  • git-branch : write-back-method: gitの際にどのようにイメージタグ変更のブランチを作成するかを示します。develop:image-updater-{{.SHA256}}の場合、developブランチから派生させ、image-updater-{{.SHA256}}(SHA256でブランチ名を重複しないように)という名前のブランチを作成します。

↓ (参考)Image Updaterのannotationsを追加したApplicationリソース例(開発環境・ステージング環境)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sample-api
  namespace: argocd
  # Image Updater用のanotations
  annotations:
    argocd-image-updater.argoproj.io/write-back-method: argocd
    argocd-image-updater.argoproj.io/image-list: container=asia-northeast1-docker.pkg.dev/sample/sample-api:latest
    argocd-image-updater.argoproj.io/container.pull-secret: pullsecret:argocd/image-updater-secrets
    argocd-image-updater.argoproj.io/container.update-strategy: digest
    argocd-image-updater.argoproj.io/container.platforms: linux/amd64

~~~ 略 ~~~

↓ (参考)Image Updaterのannotationsを追加したApplicationリソース例(本番環境)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sample-api
  namespace: argocd
  # Image Updater用のanotations
  annotations:
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: kustomization
    argocd-image-updater.argoproj.io/git-branch: develop:image-updater-{{.SHA256}}
    argocd-image-updater.argoproj.io/image-list: container=asia-northeast1-docker.pkg.dev/sample/sample-api:latest
    argocd-image-updater.argoproj.io/container.pull-secret: pullsecret:argocd/image-updater-secrets
    argocd-image-updater.argoproj.io/container.update-strategy: latest
    argocd-image-updater.argoproj.io/container.ignore-tags: latest
    argocd-image-updater.argoproj.io/container.platforms: linux/amd64

~~~ 略 ~~~

実装のハマりやすいポイント

導入背景から実装概要までお話ししてきました。しかし、実装においては決してスムーズは進まず、試行錯誤の連続でした。 MIRAIZでは、ArgoCDに関連するドキュメントをMIRAIZ ArgoCD Docsという形でesaにまとめています。ArgoCD Docsにまとめられている実装のハマりやすいポイントを共有します。実装のハマりやすいポイントは、実装時のArgoCDバージョン2.9系・Image Updaterバージョン0.12系の時に発生したものになります。

esaにまとめているArgoCD Docs

Image Updaterがコンテナイメージを検知しない

Image Updaterのupdate-strategyを指定する際には全環境でlatestを使う予定でした。 しかし、動作検証をしている中で、Image Updaterが新しいコンテナイメージを検知しないという問題が発生しました。

公式ドキュメントに以下の記載があります。

Please note that all update strategies except digest assume tags to be immutable and that new images will be pushed with a new, unique tag. If you want to update to mutable tags (e.g. the commonly used latest tag), you should use the digest strategy.

つまり、「digest以外のupdate-strategyは、イメージタグが一意である必要があり、重複したタグを使いたいのであれば、digestを使うべき」というものでした。

MIRAIZでは、開発者が事前検証のため、GitHub Actionsのworkflow_dispatchを利用して、ブランチ名を指定して開発環境用にコンテナイメージをビルド&プッシュするというケースがあり、少なからずイメージタグが重複する可能性がありました。そのため、開発環境とステージング環境は、update-strategydigestにすることで解決しました。

update-strategyがlatestの時に同一のイメージタグは再検知されない

Deploymentでreplicasを指定するとHPAが動作しない

ArgoCDの管理下にある場合、Deploymentでreplicasを指定していると、HPAによってreplicasが増加しても、負荷に関係なくDeploymentのreplicasで指定した数に戻されてしまいます。

公式ドキュメントに以下の記載があります。

It may be desired to leave room for some imperativeness/automation, and not have everything defined in your Git manifests. For example, if you want the number of your deployment's replicas to be managed by Horizontal Pod Autoscaler, then you would not want to track replicas in Git.

つまり、マニフェストで全てを管理するのではなく、自動化の余地を残すことが望ましい場合があります。その場合は、今回の様に明示的に指定しないほうが良さそうです。

↓ (参考)Deploymentのreplicasは指定しない

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: sample-api
spec:
  selector:
    matchLabels:
      app: sample-api
  # replicas: 3 HPA時にArgoCDの同期処理によって3に戻されるため、replicasは指定しない
  template:
    metadata:
      labels:
        app: sample-api
        
        
~~~ 略 ~~~

Applicationリソース適用時にHPAがOutOfSyncのループを繰り返す

Applicationリソースを適用した際に、HPAがOutOfSyncのループを繰り返す現象に遭遇しました。 Issueに記載されている通り、cpuとmemoryの位置を逆にすることで解消できました。

↓ (参考)OutOfSyncのループを繰り返すマニフェスト

    - type: Resource
      resource:
        name: cpu # ここ
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory # ここ
        target:
          type: Utilization
          averageUtilization: 70

↓ (参考)成功するマニフェスト

    - type: Resource
      resource:
        name: memory # ここ
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: cpu # ここ
        target:
          type: Utilization
          averageUtilization: 70

ExternalSecretで追加した環境変数がDeploymentで読み込まれない

ESOは、ExternalSecretというCRD(Custom Resource Definition)で使用する環境変数を定義します。しかし、ExternalSecretで定義した環境変数は、Deploymentなどのリソースから直接参照できません。代わりに、ExternalSecretで定義した環境変数をConfigMapやSecretに書き出し、Deploymentなどのリソースから参照する形になります。

↓ (参考)ExternalSecretで定義した環境変数をConfigMapで扱う

apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-api-service-env-config
data:
  app.env: |
    BASE_URL={{ .base_url }}
    EXTERNAL_API_BASE_URL={{ .external_api_base_url }} # 追加
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: sample-api-service-env-external-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: api-cluster-secret-store
    kind: ClusterSecretStore
  target:
    name: sample-api-service-env-external-secret-config
    template:
      engineVersion: v2
      templateFrom:
        - configMap:
            name: sample-api-service-env-config
            items:
              - key: app.env
  data:
    - secretKey: base_url
      remoteRef:
        key: SERVICE_SETTING
        property: BASE_URL
    - secretKey: external_api_base_url # 追加
      remoteRef:
        key: SERVICE_SETTING
        property: EXTERNAL_API_BASE_URL

↓ (参考)ConfigMapで定義したapp.envをDeploymentから参照する

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-api
  namespace: default
spec:
  selector:
    matchLabels:
      app: sample-api
  # replicas: 3 HPA時にArgoCDの同期処理によって3に戻されるため、replicasは指定しない
  template:
    metadata:
      labels:
        app: sample-api
    spec:
      containers:
~~~ 略 ~~~
      volumes:
        - name: app-env
          secret:
            secretName: api-service-env-external-secret-config # ここで参照
            items:
              - key: "app.env"
                path: "app.env"

通常、ExternalSecretとConfigMapだけ更新しても、Deploymentで管理されているPodは環境変数を読み込みません。Pod自体を再起動する必要があります。本記事では、再起動する方法については詳しく触れません。

今回、ArgoCD検証中に遭遇した問題は、以下3つの差分を同時に同期することを前提とします。

  • Deployment: コンテナイメージの更新が入る(プログラム上でEXTERNAL_API_BASE_URLを読み込む処理を追加)
  • ConfigMap: EXTERNAL_API_BASE_URLの追加
  • ExternalSecret: EXTERNAL_API_BASE_URLの追加

3つの差分を同時に同期した際には、問題なくPodは起動しました。 しかし、外部APIにリクエストが送信できず、エラーが頻発する問題に遭遇しました。調査を進めると、EXTERNAL_API_BASE_URLが正しく読み込めていないことが判明しました。 Podを再起動することで、EXTERNAL_API_BASE_URLを読み込み、正常に外部APIにリクエストを送信できるようになりました。

Podを再起動することで解決はしたものの、今回遭遇した問題は、本番環境の場合かなりリスクがあるため、詳細に調査を進めました。 調査を進めると、公式ドキュメントに同期に関して以下の記載がありました。

When Argo CD starts a sync, it orders the resources in the following precedence: ・The phase ・The wave they are in (lower values first for creation & updation and higher values first for deletion) ・By kind (e.g. namespaces first and then other Kubernetes resources, followed by custom resources) ・By name

つまり、同期には順序があるということになります。 phase → wave → kind → nameの順序でリソースが同期する優先度が決定されます。 では、それぞれどういったものなのでしょうか?詳細を確認してみます。

phase

phaseは、いくつかの適用フェーズ分ける仕組みを提供します。 公式ドキュメントには、PreSyncSyncSyncFailPostSyncが定義されています。

  • PreSync : Sync前に実行される(アプリケーションをデプロイする前にデータベースに操作を加える際などに利用)
  • Sync : マニフェストを適用する際のデフォルト
  • SyncFail : 同期が失敗した際に実行される
  • PostSync : すべての同期が完了した際に実行される

phaseを制御したいマニフェストのannotationsにargocd.argoproj.io/hookを追加することで、制御が可能です。

↓ (参考)argocd.argoproj.io/hookでphaseの制御が可能

metadata:
  annotations:
    argocd.argoproj.io/hook: PreSync

wave

waveは、phaseの中で、適用するマニフェストの順序を制御する仕組みを提供します。 waveを制御したいマニフェストのannotationsにargocd.argoproj.io/sync-waveを追加することで、制御が可能です。値は、文字列の数字で定義し、小さいほど先に同期され、デフォルトでは"0"となっています。

↓ (参考)argocd.argoproj.io/sync-waveでwave制御が可能

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"

kind

phaseとwaveが同等な場合、リソースのkindで同期する順序が決定されます。具体的には、ArgoCDの実装部分を見るのが早いです。公式ドキュメントにも記載がありましたが、CRDは一番最後になるようです。

name

phaseとwaveとkindが同等な場合、最終的にはリソースのnameで順序が決定されます。

同期順序まとめ

今回の問題は、kindによる順序制御でExternalSecret(つまりCRD)よりも先にDeploymentが同期されてしまい、追加した環境変数が読み込めなかったことに起因します。 よって、kindではなく、waveで制御する必要があります。

以下の2ケースを想定し、waveによる順序制御が可能か確認しました。

ExternalSecretを一番先に同期するケースでは、デフォルト同様に追加した環境変数は読み込めませんでした。ConfigMapを一番先に同期し、次にExternalSecretを同期するケースでは、正常に追加した環境変数を読み込めました。

そこで、関連リソースはケース2のようなwaveによる順序制御を導入することとで、問題を解決しました。

同期順序の検証ケース

まとめ

ArgoCDの導入によって、リリース作業時間が最大2時間から10分に改善しました。また、各リポジトリで分散管理されていたマニフェストも1つのマニフェスト管理用のリポジトリに集約でき、管理や変更の追跡が容易になりました。

運用開始から半年ほど経過しましたが、大きな問題は起きておらず、導入した価値があったと感じています。

alt

増田 圭佑 Keisuke Masuda

MIRAIZ開発部 MIRAIZ開発グループ

大学時代は情報科学を専攻し、2023年にパーソルキャリア株式会社に新卒入社。現在はMIRAIZの共通基盤チームで主に運用を担当している。

※2024年7月現在の情報です。