スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

ArgoCD と Renovate によるコンポーネントの継続的なデプロイ

こんにちは、SRE の @int128 です。

Quipper では日本やグローバル向けのサービスをそれぞれの Amazon EKS クラスタで提供しています。Service Level を保ちながらクラスタを運用していくには Cluster Autoscaler や Datadog Agent などのコンポーネントが必要不可欠になります。また、Developer Productivity を改善していくために内製ツールで GitHub や CI などのメトリクスをモニタリングする取り組みを進めています。Quipper ではこのようなシステム共通のコンポーネントを System Components と呼んでいます。

Service Level や Developer Productivity を継続的に改善していくには、コンポーネントの設定変更を素早く試したり、新しいコンポーネントを簡単に導入したりできる環境が不可欠です。本記事では SRE が直面した課題と ArgoCD や Renovate による解決策を紹介します。

課題と解決策

これまで System Components のデプロイは CircleCI 上でシェルスクリプトを実行していました。System Components の数が少なく構成がシンプルな場合、このような CI Ops と呼ばれる方法で十分でした。しかし、Quipper では Cluster Autoscaler や Horizontal Pod Autoscaler などの導入を経て System Components の数が大きく増加しました。

例えば、日本向けのスタディサプリの開発で利用している staging クラスタでは、System Components の数は以下のように推移してきました。

  • 2020年3月時点: 9件
  • 2020年6月時点: 13件
  • 2020年9月時点: 15件
  • 2020年12月時点: 22件(GitOps導入後)

SRE が管理しているクラスタは計4つあり、クラスタごとに System Components の構成は少しずつ違います。例えば、staging クラスタではリソース効率を改善するために試験的に Vertical Pod Autoscaler を導入しています。また、先日 @chaspy が紹介した HPA External Metrics による Scheduled Scaling の仕組みはグローバル向けのクラスタでのみ運用しています。

複雑に増加し続ける System Components をこれまでの手法で管理するのは限界を迎えていました。そこで、マニフェストの構成やデプロイの仕組みを改善することにしました。

これまでのシェルスクリプトによるデプロイでは、以下のような課題がありました。

  1. 時間の経過とともに条件分岐や kustomize patch が増えて、デプロイスクリプトの保守性が悪化してきた
  2. デプロイスクリプトからリソースを削除した場合やリソースの名前を変更した場合は、手作業で kubectl delete を実行する必要がある
  3. コンポーネントのバージョンを自動的に上げる仕組みがないため、古いバージョンのコンポーネントを使い続けてしまう

まず、課題1を解決するためにマニフェストの共通化をやめてクラスタ単位でマニフェストを管理することにしました。YAMLファイルが大幅に増えるデメリットはありますが、条件分岐やパッチを読み解く負担がなくなることやクラスタ単位の設定変更が容易になることのメリットが大きいと考えました。具体的には、以下のようなディレクトリ構成でマニフェストを管理しています。

system-components
└── overlays
    ├── CLUSTER_NAME
    │   ├── COMPONENT_NAME
    │   └── ...
    └── ...

overlays ディレクトリにはクラスタごとにマニフェストを配置します。共通部分を作るとクラスタ単位での変更が難しくなるため base は作らないルールにしています。今のところ base の必要性を感じる場面はありません。

課題2は GitOps への移行で解決できます。また、課題3は GitOps と Renovate を組み合わせることで解決できます。

これらの解決策を詳しく説明していきます。

GitOps への移行

Quipper ではすでに ArgoCD を導入しており、アプリケーションの GitOps 移行に取り組んでいます。System Components についても同様に ArgoCD を利用することにしました。

GitOps への移行を始めた9月時点では、4クラスタで合計59件のコンポーネントがありました。コンポーネントの数が多いことや日々の運用で継続的な変更があることを考えると、マニフェストの更新を止めてすべてのコンポーネントを一度に GitOps に移行することは不可能でした。

そこで、コンポーネントごとに以下の手順を繰り返すことにしました。

  1. Git リポジトリの topic branch にマニフェストを追加する
  2. ArgoCD の Application リソースを追加する。この時、Application リソースの参照先を topic branch にして、auto sync を無効にしておく
  3. ArgoCD でクラスタマニフェストの差分を確認する
  4. Git リポジトリで Pull Request を作成し、レビューをお願いする
  5. レビュー後に Pull Request をマージすると、ArgoCD によってデプロイされる
  6. デプロイスクリプトからコンポーネントを削除する

後述する IAM Roles for Service Accounts の導入など、GitOps への移行でコンポーネントの構成が変わる場合は、手順3でマニフェストを sync して動作確認しながら進めました。

また、チームで移行作業に取り組めるように先ほどの手順を Migration Guide にまとめて共有しました。最初のクラスタ@int128 が試行錯誤しながら進めましたが、残りの3クラスタについては @chaspy @rbmrclo が協力してくれました。

GitOps への移行にあたっては以下の技術的課題を考慮しました。

1. クレデンシャルの取り扱い

これまでのデプロイスクリプトでは CircleCI の環境変数を通してクレデンシャルを受け取っていました。例えば Datadog の API キーがあります。GitOps への移行にあたっては、クレデンシャルを別の仕組みで管理する必要があります。

Quipper ではすでにアプリケーションのクレデンシャルを aws-secret-operator で管理しています。System Components のクレデンシャルについても aws-secret-operator に移行することにしました。これでクレデンシャルを安全にデプロイできます。

AWS API を利用するコンポーネントではこれまで IAM Access Key を利用してきました。しかし、aws-secret-operator が利用する IAM Access Key を aws-secret-operator で管理する場合、誰が最初に IAM Access Key をクラスタにデプロイするのかという問題があります。このような鶏と卵の問題や、クレデンシャルの漏洩リスクを踏まえて、この機会にすべての System Components を IAM Roles for Service Accounts (IRSA) に移行することにしました。IRSA については別の記事で紹介する予定です。

2. Helm chart の取り扱い

Datadog Agent などの一部のコンポーネントでは Helm chart をデプロイしています。ArgoCD で Helm chart をデプロイするには以下の選択肢があります。

前者は構成管理がシンプルです。一方で、後者の場合、実際にデプロイされるマニフェストを Pull Request で確認できます。Quipper では Pull Request のレビューフローが定着しており、後者のメリットが大きいと考えました。

Helm chart の管理には Helmfile を利用しています。Helmfile を利用すると、chart の URL やバージョンを YAML ファイルで宣言的に管理できます。また、Renovate が Helmfile に対応しているため、継続的なバージョンアップが可能です。

コンポーネントの設定を変更する場合は以下のような作業フローになります。

  1. コンポーネントが利用する Helm chart の values.yaml を修正する
  2. make コマンドでレンダリング済みマニフェストを更新する
  3. Pull Request を作成する
  4. Pull Request をレビューする
  5. Pull Request をマージすると、ArgoCD によってデプロイされる

もし手順2を忘れた場合でも CI でレンダリング済みマニフェストが自動的に更新されるようにしています。人間はいつかミスをしてしまうので、機械がミスから守ってくれる仕組みが好きです。

レンダリング済みマニフェストの自動的更新

これで Helm chart を確実かつ継続的にデプロイしていけるようになりました。

3. App of Apps Pattern による集約管理

ArgoCD のドキュメントでは App of Apps というパターンが紹介されています。このパターンでは、子 Application リソースを束ねる親 Application リソースを定義することで、複数の Application をまとめて管理できます。

Quipper ではクラスタ単位に ArgoCD を管理しているため、クラスタ単位に親 Application リソースを定義することにしました。具体的には、以下のようなディレクトリ構成で Application を管理しています。

system-components
└── overlays
    ├── staging-cluster-01
    │   ├── bootstrap
    │   │   └── system-components.yaml  # 親 Application リソース
    │   ├── applications
    │   │   ├── argocd.yaml             # 子 Application リソース
    │   │   └── ...                     # 子 Application リソース
    │   ├── argocd
    │   │   ├── kustomization.yaml      # ArgoCD のマニフェスト
    │   │   ├── deployment.yaml         # ArgoCD のマニフェスト
    │   │   └── ...

親 Application では applications ディレクトリを参照するようにします。このように設計することで、ArgoCD は applications ディレクトリにある Application リソースをすべてデプロイしてくれます。また、applications ディレクトリに新しい Application リソースを追加すると、ArgoCD が変更を検知してデプロイしてくれます。

ArgoCD Server では以下のように表示されます。

ArgoCDの画面

過去に @d-kuro が App of Apps Pattern を検討していたこともあり、ディレクトリや Application リソースの構成については彼と議論しながら設計を進めました。

4. クラスタの初期化

EKS クラスタを初期化するには以下のコンポーネントが必要です。

  • aws-secret-operator(ArgoCD のクレデンシャルを管理するため)
  • ArgoCD
  • 親 Application

これらのコンポーネントをデプロイすれば、ArgoCD が子 Application を自動的にデプロイしてくれます。

では、これらのコンポーネントをどんな手段でデプロイすればよいでしょうか。Quipper では AWS CodeBuild で Terraform を実行して AWS リソースを管理していることを踏まえると、以下の方法が考えられます。

Terraform 定義ファイルと Kubernetes マニフェストファイルはそれぞれ別のリポジトリで管理しているため、後者を選択しました。

最終的に、新しいクラスタを構築する手順は以下のようになりました。

  1. SRE が Terraform で 新しい EKS クラスタを追加するための Pull Request を作成する
  2. SRE が Pull Request をマージする
  3. AWS CodeBuild で terraform apply が実行されて、自動的に以下のリソースが作成される
  4. SRE はクラスタ初期化用の CodeBuild Project を手動実行する
  5. AWS CodeBuild で kubectl apply が実行されて、最小限のコンポーネントがデプロイされる
  6. ArgoCD で残りのコンポーネントがデプロイされる

Renovate による継続的なバージョンアップ

Quipper では Renovate というサービスを利用しています。Renovate はライブラリやミドルウェアなどのバージョンアップを Pull Request で通知してくれるサービスです。例えば、ingress-nginx というコンポーネントの新しいバージョンがリリースされると、以下のような Pull Request が作成されます。

Renovate による Pull Request

これはどんな仕組みで実現しているのでしょうか。実は kustomization.yamlhelmfile.yamlリポジトリに置いておくだけで、Renovate が自動的に Pull Request を送ってくれます。先ほどの例だと helmfile.yaml を以下のように変更する Pull Request が作成されます。

 repositories:
   - name: ingress-nginx
     url: https://kubernetes.github.io/ingress-nginx
 releases:
   - name: ingress-nginx
     chart: ingress-nginx/ingress-nginx
-    version: 3.13.0
+    version: 3.15.2
     values:

Renovate を効果的に活用するには設定に工夫が必要です。System Components のバージョンアップはクラスタ単位で行いたい(基本的に staging で動作確認してから production に展開する)ので、Renovate ではクラスタ単位で Pull Request が作成されるように設定しています。このへんは @suzuki-shunsuke が快適な環境を整備してくれました。詳しくは Renovate の Tips をご覧ください。

まとめ

System Components のデプロイを GitOps に移行したことで以下の効果があったと考えています。

まず、手続き型から宣言型のデプロイ方式に変わり、クラスタにどんなマニフェストが配置されているか分かりやすくなりました。これにより、設定の変更や新しいコンポーネントの追加が簡単にできるようになりました。結果的に、Service Level や Developer Productivity を早いサイクルで改善していくための基礎ができました。

また、リソースの削除や名前変更を行った場合も ArgoCD が確実にリソースを削除するため、クラスタにゴミが残らないようになりました。kubectl apply によるデプロイでは不要なリソースを手作業で削除する必要がありましたが、ついつい忘れてしまうことがありました。クラスタをきれいな状態に保つことで、認知負荷を軽減する効果があったと考えています。

GitOps に加えて Renovate を利用することで、コンポーネントのバージョンアップが簡単かつ継続的に行えるようになりました。これにより、セキュリティパッチや新機能への対応が早くなったと感じています。

Quipper では世界の果てまで学びを届けたい仲間を募集しています。なお、筆者が所属する SRE Team も募集中です。カジュアル面談や応募をお待ちしています。