アプリケーションの開発フローとGitOps
アプリケーションの開発フローとKubernetesへのデプロイを考えた軌跡を残します。特に結論はないです。
前提
以下を前提とする。
- Kubernetesにアプリケーションをデプロイする場合を考える。
- すべての変更はPull Requestを通して適用する。
- アプリケーションはソースコードを変更することなく、どの実行環境でも動作可能とする。
- アプリケーションの設定は実行時に環境変数などで注入される。
- 開発チームはアプリケーションと設定に責任を持ち、自分自身でデプロイを行う。
- Platformチームは実行環境の健全性に責任を持つ。
以下はスコープ外とする。別に考える必要がある。
- 実行環境自体のデプロイや変更(例えば、Kubernetesのバージョンアップ)
- アプリケーションが依存するステートフルなリソース
- データベーススキーマのマイグレーション
- アプリケーションやイメージのセキュリティ検査
- などなど
選択肢
以下の選択肢が考えられる。ここでは具体的なツールは考えない。
- ビルドとデプロイ
- アプリケーションのビルドとデプロイを分離する
- アプリケーションのビルドとデプロイを連続して行う
- ブランチ戦略
- tagから実行環境にデプロイする
- masterブランチから実行環境にデプロイする
- 実行環境ごとにブランチを用意する(例えば、GitLab Flow)
- イメージタグの管理
- アプリケーションとマニフェスト
デプロイサイクルの速いチームは、(a-2)のようにブランチの更新を契機にアプリケーションをデプロイした方が無駄な作業が少なくなる。そうしないとdeveloper experienceが大幅に悪化するだろう。一方で、本番環境で問題が発生してすぐに旧バージョンに切り戻したい場合は、(a-1)のようにデプロイを切り離しておく方が早く確実に対応できる。
a, b, c, dには以下の制約がある。
- アプリケーションのビルドとデプロイを連続して行う場合(a-2)
以降は具体的なツールを挙げて比較してみよう。
(a-1)(b-1)(c-1)(d-1)
GitOpsを採用する場合に最も簡単なフローから考えてみよう。デプロイの頻度が低いチームはこのフローで十分と思う。
- 開発者がアプリケーションリポジトリに新しいタグをpushする。
- CIが新しいイメージをビルドする。リポジトリのタグとイメージのタグは名前を合わせておく。
- 開発者がマニフェストリポジトリを更新するPRを作成する。
- 開発者がマニフェストリポジトリのPRをマージする。
- GitOpsツールがマニフェストリポジトリとクラスタを同期させる。
メリデメ:
- 👍 CI/CD外でリソースが変更されても不整合が起こらない。
- 👍 旧イメージタグへの切り戻しが確実で早い。
- 😱 デプロイに必要な作業が多すぎる。
(a-1)(b-2)(c-1)(d-1)
前項からdeveloper experienceを改善するためにイメージタグの更新を自動化する。イメージタグはcommit hashから生成する。
- 開発者がアプリケーションリポジトリのmasterブランチを更新する。
- CIが新しいイメージをビルドする。
- CIがマニフェストリポジトリのイメージタグを更新するPRを作成する。
- 開発者がマニフェストリポジトリのPRをマージする。もしマニフェストを変更したい場合はPRと合わせて更新する。
- GitOpsツールがマニフェストリポジトリとクラスタを同期させる。
メリデメ:
- 👍 開発者はPRをマージするだけで新イメージタグをデプロイできる。
- 👍 CI/CD外でリソースが変更されても不整合が起こらない。
- 👍 旧イメージタグへの切り戻しが確実で早い。
- 😱 マニフェストリポジトリの更新やPRの作成を作り込むのが大変そう。
(a-2)(b-3)(c-1)(d-2)
前項ではPRを挟んでイメージタグを更新していた。ビルドとデプロイを連続して行いたい場合(a-2)、アプリケーションとマニフェストをアトミックに更新できないことが課題となる。そこで、アプリケーションとマニフェストを同じリポジトリで管理する(d-2)。
アプリケーションとマニフェストのライフサイクルが合わなくなるので、実行環境ごとにブランチを用意する(b-3)。
- 開発者がリポジトリの (master | staging | production) ブランチに対するPRをマージする。
- CIが新しいイメージをビルドする。アプリケーションリポジトリに含まれるマニフェストをレンダリングする。
- CIがマニフェストリポジトリを更新する。
- GitOpsツールがマニフェストリポジトリとクラスタを同期させる。
メリデメ:
- 👍 開発者は実行環境に対応するブランチを更新するだけで新イメージタグをデプロイできる。
- 👍 CI/CD外でリソースが変更されても不整合が起こらない。
- 😱 切り戻す場合はアプリケーションのrevert commitが必要になる。
- 😱 マニフェストの変更だけでもイメージが新しくなってしまう。
- 😱 マニフェストリポジトリの更新やPRの作成を作り込むのが大変そう。
(a-2)(b-3)(c-2)(d-2)
GitOpsを使わない方法も考えてみよう。まずは、アプリケーションとマニフェストを同じリポジトリで管理する。
- 開発者がリポジトリの (master | staging | production) ブランチに対するPRをマージする。
- CIが新しいイメージをビルドする。
- CIがhelm upgradeを実行する。
もしビルドとデプロイを切り離したい場合(a-1)は、CIの手動ジョブを使えばよい。
メリデメ:
- 👍 開発者は実行環境に対応するブランチを更新するだけで新イメージタグをデプロイできる。
- 👍 CIで完結する。複数ツールのピタゴラスイッチがない。
- 👍 緊急の切り戻しが必要な場合はリソースを直接変更できる。(直接変更できてしまうデメリットでもある)
- 😱 新しいマニフェストにないリソースを確実に削除するため、Helmが必須になる。他のツールは使えない。
- 😱 CI/CD外でリソースが変更されると不整合が発生する。
- 😱 マニフェストの変更だけでもイメージが新しくなってしまう。
- 😱 リポジトリとクラスタの差分を確認することは可能(helm-diffを利用)だが、Argo CDのような分かりやすさはない。
(a-1)(b-2)(c-2)(d-1)
アプリケーションとマニフェストを別のリポジトリで管理する方法もあるが、GitOpsを利用する場合とほぼ同じフローで、わざわざHelmを利用するメリットは小さい。
まとめ
思いつくままに挙げていったところこんな感じです。誰かカルノー図で組み合わせ網羅をお願いします😓
ちなみに、EC2 + Auto Scaling Group + Immutable Infrastructureなサービスの開発チームにいた時は以下のフローでやっていました。
- 開発者がアプリケーションリポジトリに新しいタグをpushする。
- CIが新しいTar ballをビルドしてS3に配置する。
- 開発者がタグ名を指定してデプロイツールを実行する。(社内の内製ツール)
- デプロイツールがAuto Scaling GroupのLaunch Configurationを更新する。
- デプロイツールがEC2 Instanceをterminateする。
一方で、少人数で速いサイクルの開発を行っているチームでは以下のようなフローでした。
- 開発者がアプリケーションリポジトリのmasterブランチにpushする。
- CIが開発環境にデプロイする。
- 開発者が後続の手動ジョブを実行する。
- CIが本番環境にデプロイする。
というわけでオチは特にないです。