はじめに
はじめまして!!!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な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 + 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の場合、本番環境で使いたいパターンです。
write-back-method: argocd
最新のイメージを検知し、内部的にイメージタグを変更するパターンです。マニフェスト上でイメージタグは管理しないため、リソースをクラスタから削除して再作成すると、Image Updaterによって行われた変更は失われます。MIRAIZの場合、開発環境とステージング環境で使いたいパターンです。
実装概要
ArgoCD + Image Updaterの構成の重要なポイントを挙げます。
導入
ArgoCD、Image Updater共にHelmでクラスタに導入しました。
ArgoCD管理画面
ArgoCDの管理画面は、Ingressリソースにargocd-serverを紐づけて、アクセス可能にしています。追加でGoogle CloudのIdentity-Aware Proxy(IAP)も導入することで、管理画面自体を特定ユーザーのみ閲覧可能にしています。
↓ (参考)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のコンテナイメージを検知する際の方法を示します。digest
やlatest
などがあります。開発環境・ステージング環境と本番環境で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系の時に発生したものになります。
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-strategy
をdigest
にすることで解決しました。
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は、いくつかの適用フェーズ分ける仕組みを提供します。
公式ドキュメントには、PreSync
やSync
、SyncFail
、PostSync
が定義されています。
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つのマニフェスト管理用のリポジトリに集約でき、管理や変更の追跡が容易になりました。
運用開始から半年ほど経過しましたが、大きな問題は起きておらず、導入した価値があったと感じています。
増田 圭佑 Keisuke Masuda
MIRAIZ開発部 MIRAIZ開発グループ
大学時代は情報科学を専攻し、2023年にパーソルキャリア株式会社に新卒入社。現在はMIRAIZの共通基盤チームで主に運用を担当している。
※2024年7月現在の情報です。