🐙

SpinnakerからArgo CDへ移行した話

2022/12/10に公開

この記事は Kubernetes Advent Calendar 2022 の 11日目の記事です。

Overview

DMM.comでSREエンジニアをしているjunkudoです。

私の現在所属するチームでは120人規模の開発組織にマイクロサービス基盤を提供しています。
マイクロサービス基盤としては、マルチテナントのKubernetesクラスタを提供しており、
利用者の開発効率向上の目的で、それに付随した様々なエコシステムを提供しています。

今回はその中でCDのエコシステムとして利用していたツールをSpinnakerからArgo CDへ移行した話について、そしてArgo CDに移行して半年以上運用した所感について語っていきます。

Spinnakerとは?

Spinnakerは元々Netflix(+ Google)によって開発されているOSSです。
公式ドキュメントでは高いベロシティと信頼性でのリリースをサポートしてくれるマルチクラウドにも対応したCDプラットフォームとされています。

Spinnaker is an open-source, multi-cloud continuous delivery platform that helps you release software changes with high velocity and confidence.

Argo CDと大きく異なる点として、Kubernetes以外での利用も想定された作りになっています。
そのためGoogle Compute EngineやAmazon ECSなどへのデプロイにも対応しています。
※ただし今回のケースでは、あくまでKubernetesクラスタへのCDツールとしての役割のみで活用していました。

どのように使うのか?

ざっくり利用方法を説明すると、アプリケーションごとに実施したいデプロイ方法をPipelineとして定義します。下記のPipelineではstageとしてConfiguration, Manual Judgement, Deploy(Manifest) という3つのステップで構成されています。

PipelineのConfiguration画面
PipelineのConfiguration画面

それぞれのstageの役割としては以下の通りです。

  • Cofiguration: 何をトリガーにPipelineを起動するのか、並列実行を許容するかなどの設定を行います。例えばContainer RegistryへのImageのpushやWebhookをトリガーとすることができます。
  • Manual Judgement: 人間がGUIからApproveを出すまで次ステップの実行をブロックします。
  • Deploy(Manifest): 設定通りにKubernetesクラスタにデプロイします。
    どのようなKubernetesマニフェストをデプロイするかも基本はここで指定できます。このKubernetesマニフェストは特定のRemote Repositoryを参照して持ってくることも可能です。

Spinnakerではアプリケーションのデプロイ戦略ごとにこのようなPipelineを定義して、リリースを自動・手動実行することができます。

実際に弊チームでは次のような形式でSpinnakerを利用していました。
アーキテクチャ上ではSpinnakerと一括にされていますが、実際はclouddriverやgateなど複数のマイクロサービスによって構成されています。

Spinnaker Architecture
実際にSpinnakerを利用していた際のArchitecture

Spinnakerの選定/廃止理由

廃止理由1: ビルトイン機能がユースケースと合わず作り込みが必要だった

Spinnakerは機能自体は多いものの、実際かゆいところに手が届かないことが多々ありました。

例えばデプロイ前のマニフェストに何かしらのパッチや変更を当てたいというシナリオを考えてみます。
このシナリオを達成できれば、例えばデプロイを実施したタイムスタンプをラベルとして付与したり…と何かしらの使い方ができそうです。
DatadogではKubernetesマニフェストに特定のバージョンを書き込むことで、トレースやログ情報に対してバージョンのタグを埋め込むことができます。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    tags.datadoghq.com/env: "<ENV>"
    tags.datadoghq.com/service: "<SERVICE>"
    tags.datadoghq.com/version: "<VERSION>"
...
template:
  metadata:
    labels:
      tags.datadoghq.com/env: "<ENV>"
      tags.datadoghq.com/service: "<SERVICE>"
      tags.datadoghq.com/version: "<VERSION>"

Spinnakerでは"Patch Kubernetes Manifests"というStageで、リソースにPatchを当てることが出来ます。
https://spinnaker.io/docs/guides/user/kubernetes-v2/patch-manifest/

一見使えそうなStageですが、実はデプロイ済みの実リソースにはPatchをあてることができる一方、デプロイする前のマニフェストにPatchを当てることが出来ません。
そのため上記のようなDatadogのバージョンタグをSpinnakerで付与してデプロイしたい、といったケースでは一旦Deploymentのデプロイをした後にパッチを当てる都合で
Deploymentの更新が2回入ることになってしまいます。

さらにSpinnakerではPatch対象として、カスタムリソースを選択することが出来ません。
カスタムリソースを扱うようにするためには、Javaでハンドラを記述してclouddriverというコンポーネントの独自のバージョンを用意する必要があるようです。
https://spinnaker.io/docs/guides/developer/extending/crd-extensions/

マニフェストへのパッチは出来ないようなので、Spinnakerの別の機能を検討してみます。
SpinnakerではSpring Expression Language (SpEL)を使うことでマニフェストにプレースホルダーをどこにでも仕込むことができます。
https://spinnaker.io/docs/guides/user/kubernetes-v2/parameterize-manifests/
イメージとしては以下のようになります。

# ... 
spec:
  replicas: '${ #toInt( parameters.replicas ) }'
# ...

ただしこれを利用すると、プレーンなyamlファイルではなくなるのでマニフェストを管理しているリポジトリ側のCIがvalidationエラーを起こす可能性があります。またSpinnakerを通さないとデプロイ出来ないマニフェストになってしまうので、ポータビリティが損なわれてしまいます。

以上のようにユースケースにはまる機能が案外ないという印象でした。

このビルトイン機能の不足を解消しようとすると益々Spinnakerにロックインされていきます。
詳細は省きますが他にも以下のような都合があったりします。

  • 様々なデプロイ戦略(Blue/Green, Canary Release)を実施できる
    → デプロイ対象はDeploymentリソースではなくReplicasetリソースを利用する必要があります。
  • Pipelineのstageとして任意の処理をリリース時に実行できる
    → 別途Jenkinsを導入してSpinnakerと連携させる必要があります。
  • Jobを実行してその結果をキャプチャして次Stageに受け渡すことができる
    → 特定のログ出力ルールに従う必要があります。具体的にはSPINNAKER_PROPERTY_HOGE=FOOという形式で出力したものだけ取り込んでくれます。
    https://spinnaker.io/docs/guides/user/kubernetes-v2/run-job-manifest/#spinnaker_property_

弊チームではKubernetesクラスタをKaaSとしてマルチテナントで提供しています。
KaaS利用者の各チームにもSpinnakerを利用してもらうことになるので、機能の複雑な制限・使用方法を把握して利用してもらう、
もしくはSREチーム側でこの複雑な制限・使用方法を抽象化する仕組みを用意しなければなりませんでした。
こういった都合から、Spinnaker周りの作り込みを進めるとKaaS利用チームの認知不可・SREチームの運用負荷が高くなる恐れがありました。

廃止理由2: 運用コストが高かった

ノイジーネイバー問題

Spinnakerのコンポーネントが特定ノードでDisk I/Oを詰まらせることで、同一ノードにいるアプリケーションの性能を悪化させました。
実際にこれによる影響を受けた他チームのトラブルシュートが体験談が記事になっているのでこちらを読んでいただければと思います。

https://zenn.dev/aanrii/articles/a32bc853584734#nodeの性能改善

ただこの問題は、一応Spinnaker専用のノードを立てて他アプリケーションと分離するといった対応もやろうと思えば可能ではあるので、この問題単体では廃止にする意味合いは薄いと思います。

Spinnakerの更新作業にhalyardが必要である

Spinnakerのバージョンアップや何かしらの設定変更のときにはhalyardという専用のCLIが必要になります。
これはUbuntu,DebianもしくはDockerで動くバイナリとして提供されています。
弊チームでは下記のようにクラスタ内にhalyardを動作させる専用のStatefulSetを立てて、Spinnakerの更新作業時にはそのPodに入って作業する、といった形式をとっていました。

Spinnakerの設定を行うためのファイルも別途管理しなければいけません。このファイルはPodのhalyardから参照できるようにしておく必要もあります。

公式ドキュメントが整備されていない+多くのバグが残存している

Spinnaker運用してる際は下記のようなバグ(or仕様)に遭遇しました。またドキュメント自体も情報が少ない状態となっています。
コントリビュートして解消はしたかったものの現実的に割ける工数的を考えると、あらゆるものにコントリビュートしてこれらの不具合を修正するというのが困難でした。

Spinnaker自身がマイクロサービス構成である

Spinnakerはマイクロサービス構成となっていて、10個以上のコンポーネントで構成されています。
コンポーネントが多い都合でトラブルシュートや何かしらのセットアップにはかなり手間取りました。

Spinnaker Architecture
Spinnakerのアーキテクチャ図(公式ドキュメント)

慣れの問題でもあるのですが、トラブルシュート時にはこれらの中のどれかから発生しているであろうエラーログを探し出したり、また単純に管理しなければいけないコンポーネントが多いという点も辛みでした。

コンポーネントでPodが生成され、いくつかのコンポーネントはPodを複数個用意することで冗長化できるのですが、EchoとIgorという特定のサービスは複数構成にしてはいけないといった制約があったりと差異があるので、認知負荷が高いです。

また各種トラブルシュート時にはドキュメントの情報からの情報だけでは足りず、直接Githubにアプリケーションコードを読みに行くときがあったのですが、そこもマイクロサービス毎にリポジトリが分かれて管理されていたりするので、調査しづらかった記憶があります。

Spinnakerの代わりとなるツールの選定

Spinnakerについては上記のように様々な廃止理由があったので、新しいCDツールへの移行を検討しました。
その際には以下の点を重視しました。これらの内容を満たせれば既存のSpinnakerでのユースケースを満たすことが出来たためです。

  • マルチクラスタを取り扱うことができる(=つまり複数クラスタに対してデプロイが実行できること)
  • 他社での利用実績があること
  • 作り込みが不要であること
  • Spinnakerのイメージ上書き機能を実現できること

最後の項目だけはArgo CD単体で満たすことはできませんが、それ以外の項目はArgo CDで満たすことができるため移行を進めることになりました。
Fluxなども検討はしましたが、マルチクラスタに対応していない等の都合があり今回は無しとなりました。

Argo CDとは?

Argo CDはGitOps,SSoTの思想に基づいたCloudNativeなCDツールです。
ちなみについ最近CNCFによってArgo Projectがgraduatedになりました🎉
https://twitter.com/argoproj/status/1595571860573298688?s=20&t=YEuN1XKze62pvW6r4W8O2Q

SSoTについての特性やSpinnakeのコンセプトについては以下の記事で分かりやすくまとめられているのでぜひそちらをご覧ください。
https://www.pospome.work/entry/2022/02/19/153136#Single-Source-of-TruthSSOT

実際にArgo CDを入れて感じたこと

ポジティブな感想

Spinnakerに比べてKubernetesに特化した作りになっている

Spinnakerに比べるとKubernetes周りで基本必要な機能が必要十分に揃っている印象でした。
一部印象に残っている点を例示します。

  • GUI上のSecretリソースは伏せ字にしてくれる
    全リソースのRead権限を付与していてもGUI上では以下のようにSecretリソースの値を伏せ字にしてくれます。
    こういった細かい箇所に融通が効いているのは作り込みを不要にしている点となっています。
apiVersion: v1
kind: Secret
...
data:
  key: ++++++++

基本Argo CDを運用する我々にとって、様々なカスタマイズが必要なSpinnakerと違いArgo CDのビルトイン機能でほとんどが済むといった体験が得られています。
ちなみにSpinnakerからArgoCDへの移行は、組織的には段階的な導入やサポートなど色々やったものの技術的にはSpinnakerのパイプラインを止めて、単純にArgo CDの利用手続きを進めるといったシンプルなものだったので今回は割愛します。

すべてがk8sリソースとして管理できる

SpinnakerではHalyardの管理が必要でした。
今回は詳細を省略しますが、専用のDockerImage,StatefulSet,Persistent Volume,そしてhalyard自身のバージョンを管理する必要がありました。

一方でArgo CDはArgo CD自身の設定も含めて基本ほとんどがKubernetesのリソースとして管理することが出来ます。
そのためSpinnakerの更新作業ではhalyardを別途管理してそこから行う必要があったのに対して、Argo CDの更新作業ではKubernetes ManifestをApplyするだけで済むようになりました。

案外GUIが便利

Argo CDはGUI上であらゆるリソースの状態を確認することが出来ます。
Spinnakerではデフォルトだと主にCronJob,Deployment,Serviceリソースの状態ぐらいしか観察できません。

KaaS利用チームによってはKubernetesについての習熟度が低い方もいます。
そういった方にとって、GUI上で管理リソースやそれらの状態・ログが確認できるというのはデバッグにかなり役立っているように感じます。
例えばPodがうまく立ち上がらなかったときのデバッグや、障害対応のときにも活用されています。

また、GUI上からDeploymentを再起動したりリソースを削除したりといった、操作も権限があれば可能なのでSpinnakerの時と異なり開発チームはkubectlを使って操作・調査を行うケースが減りました。

ネガティブな感想

ImageOpsが無い

もともとKubernetesマニフェストを管理しているリポジトリではGitOpsを意識した作りではありませんでした。

例えばSpinnakerではDeploymentやCronJobマニフェスト上のコンテナイメージのImageタグを
特定の正規表現にマッチしたコンテナイメージがGARにpushされたときに、そのタグを自動で付与してくれる、という機能があります。
そのためリポジトリ上のマニフェストに記述しているコンテナイメージにはタグが書かれていませんでした。

しかしArgoCDの作りはGitOpsの考えに倒れているので、こういった機能は実装されていません。

これを解決するArgo CD Image Updaterというプラグインも存在しますが、うちでは最終的にkustomizeやhelmを使わない生のマニフェストを
Applicationの参照先としている都合で利用不可でした。
https://argocd-image-updater.readthedocs.io/en/stable/

変わりの仕組みとしてGARにコンテナイメージがpushされたときに、自動でリポジトリ上のマニフェストのイメージタグを置換してくれるCIを別で用意して対応しています。

他社事例としてMercari社ではFluxの機能の一部を利用することで対応したようです。
https://engineering.mercari.com/en/blog/entry/20220201-towards-a-more-stable-and-secure-cd-system-replacement/

通知の難しさ

通知はテンプレートとトリガーを記述して、どのタイミングでどのような通知を飛ばすのか、といったことをConfigMapを通して設定することができます。

現在は下記のタイミングで通知を流すようにしています。

  • Sync(=デプロイ)を開始したとき
  • Sync(=デプロイ)が完了+成功したとき
  • Syncが失敗、もしくはSync完了後、リソースがHealthyな状態にならなくなった際

3つ目は例えばデプロイ後にPodのコンテナがうまく立ち上がらなかったとき等にも鳴ってくれる通知なのですが、たまに不要なタイミングでなることがあります。
Argo CD自身とapi-serverの通信がうまく行かなかったことでリソース情報を更新できなかったり、カスタムリソース(ex.ExternalSecrets)が一瞬だけステータスがエラーを吐くことで通知が鳴ってしまうという現象を確認しています。

特にapi-serverとの通信が一時的にうまくいかなったケースに関しては解消が難しく
3つ目の通知は仕方なく、デプロイ完了後30分以内に発生したものだけがデプロイ起因によるエラーだとして通知を流す、といったロジックにしています。

trigger.team-channel: |
  - when: app.status.operationState.phase in ['Running']
    send: [running-team-channel]
  - when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
    oncePer: app.status.operationState.syncResult.revision
    send: [succeeded-team-channel]
  - when: app.status.operationState.phase in ['Error', 'Failed']
    send: [failed-team-channel]
  - when: app.status.health.status == 'Degraded' and time.Now().Sub(time.Parse(app.status.operationState.finishedAt)).Minutes() <= 30
    send: [failed-team-channel]

まとめ

Spinnakerはもともと、単純な仮想マシンなどKubernetes以外の利用も含めたユースケースを想定しているツールでした。
様々なケースを想定されている分、ビルトイン機能が多いというよりは"カスタマイズ性"が高いソフトウェアのようになっています。
カスタマイズ性の高さから組織にあったものを作り上げることができそうな一方で、
カスタマイズするための工数はかなり高く、加えて運用工数も高い傾向がありました。

一方でArgo CDはKubernetesをターゲットとして絞ったツールで、GitOpsの考えを踏襲しており、良い意味でなんでも屋さんになっていないソフトウェアです。
現状作り込みを頑張ってせずとも動いてくれますし、何かしら設定が必要な場合はほぼConfigMapですべて済む状態になっています。

今回のケースではArgo CDのほうがSpinnakerより弊チームのユースケースにあっていたという状態でしたが仮にSpinnakerがビルトイン機能が充実していたり、CD専門のチームがあってそのチームに十分な人数がいたり、Kubernetes以外にも多種多様な実行環境や技術スタックをもっていたりしたら
Spinnakerのほうがあっているという可能性もありました。

Argo CDに移行したことで、Spinnakerにしかない機能はあるもののSREチーム自身の運用コスト、そしてGUI等の利便性の面でKaaS利用者のチームの体験を向上させてくれており、結果として移行してよかったと考えています。

Discussion