Kubernetes Advent Calendar 2019 その3 の 2日目です。
WeaveworksによってGitOpsが提案されてから2年ほどが経ち、僅かですが本番導入事例も耳にするようになりました。とはいえ案外まとまったドキュメントは作られていません。特にGitOpsで複数の環境 (e.g., 開発環境、本番環境、etc.) をハンドリングするためには欠かせないブランチ戦略については殆ど語られていないようです。これではたとえGitOpsの概要 (Single Source of Truthの概念等) を知っていても本番導入には大きなハードルが残ったままで、本番導入事例がまだまだ少ないことにも納得できてしまいます。そこでこの記事ではブランチ戦略に焦点を当て、サンプルプロジェクトを交えながら紹介していこうと思います。k8s/GitOps中級者向けです、多分。
以前GitOpsについて発表した際に端折ってしまった分 の補完にもなっています。
前提
本記事では以下のことを前提として解説せずに進めます。必要に応じて外部資料を参照してください。
なお、本記事の内容は技術書典7にて頒布した『Kubernetes-Native Development&Deployment』の一部を抜粋・結構改変したものです。全体像やサンプルプロジェクトをもっと知りたいといった場合は↓からどうぞ。GitOpsだけでなくk8s向け開発環境についても解説しています (ただの宣伝です!!!)。
概要
GitOpsでは全てがGitで管理されますが、そのブランチとアプリ実行環境への反映をどう対応付けるかを考えなければなりません。サービスによってはサービスのバージョンごとに本番環境が必要であったり、逆に本番環境は常に1つで最新にしたり、はたまたユーザごとに個別の環境を払い出すようなサービスであれば似たような環境が大量に必要になったり、と形態は様々です。更に開発のための環境や本番デプロイ前の最終確認としてステージング (=プレビュー) 環境が必要にもなるはずです。動かしたいサービスのモデルにブランチ戦略をうまく合わせない限りいい感じにデプロイサイクルを回すことはできないのです。
用語
以下は適切な用語が定義されていないため、分かりやすくするため筆者が勝手に定義した用語です。
- デプロイ要求: GitOpsの文脈において、アプリリポジトリの更新にCIがフックしSingle Source of Truthであるmanifestsリポジトリに対してその更新を反映させるPRを発行すること。具体的には、manifestsリポジトリに含まれる対象アプリのマニフェストファイルのDockerイメージタグを最新のものに変更するPRを発行する
サンプルプロジェクト
比較的汎用的と思われる形のサンプルで紹介していきます。今回は以下のような場面を想定したものとなります。サービスの要求によっては調整する必要があります。
- 本番環境 (
prd
) は1つで、常に全ユーザに同じバージョンで提供するようなサービス - 開発環境 (
dev
) は1つで、常にアプリリポジトリの最新状態がデプロイされている - 必ず先に開発環境にデプロイし、後々本番環境にデプロイする
- 出来る限りの自動化を行う
- 使用ツール
- CI: travis
- CD: argo-cd
Gitリポジトリ
サンプルプロジェクトは5つのGitリポジトリから構成されています。
manifests
アプリのSingle Source of Truthとなるリポジトリです。app-aとapp-bをデプロイするためのManifestを含んでいます。app-aはhelmを、app-bはkustomizeをテンプレートエンジンとして利用し dev
・prd
環境にパラメータ違いのデプロイを実現しています。
例示のためにhelmとkustomizeを併用していますが、実際にはどちらかに寄せたほうが良いでしょう。
system-manifests
ドメインを実現するアプリ 以外の ミドルウェアのSingle Source of Truthとなるリポジトリです。今回のサンプルではsealed-secretsをデプロイするためのManifestを含んでいます。IstioやPrometheusといったミドルウェアを利用する場合もここに含まれることになります。パラメータファイルだけを保持し、HelmのChart自体は外部を参照する構成となっています。
manifestsリポジトリとsystem-manifestsリポジトリを分けている理由ですが、これはそれぞれのリポジトリで扱っているもののライフサイクルが異なるためです。デプロイの粒度やタイミング、デプロイ先Namespaceも全く異なりますし、ブートストラップ時にはミドルウェア→アプリという順序が必要になることもしばしばあります。「Single Source of Truthといいつつ複数あるじゃねーか!」とツッコミたくなるかもしれませんが、筆者的には常に分けたほうが何かと便利という結論に達しました。
cd-manifests
CDツールであるargo-cdの設定のSingle Source of Truthとなるリポジトリです。各環境にインストールしたargo-cdからはこのリポジトリを参照させます。(argo-cdをデプロイするためのManifestではなく、) ApplicationをデプロイするためのManifestを含んでいます。ここの Application とはデプロイ設定を表す Custom Resource Definitionです。"Application of Applications" と呼ばれる、幾つかのApplicationをデプロイするためのApplicationを取る構成となっています。これは例えば、「dev
環境のアプリ」・「prd
環境のミドルウェア」といった単位でのデプロイを可能にするための手法です。リポジトリルートにApplication of Applicationsがあり、個々のApplicationは環境別・アプリ/ミドルウェア別に配置されています。
app-a
サービス実現のために動かすアプリのリポジトリです。Pythonで簡易的なAPIサーバを実装をしています。 (環境変数でDB接続情報等の開発環境/本番環境で変更が必要なパラメータを渡せるようにすることで、同一のDockerイメージで開発環境と本番環境どちらにも対応できるという前提があります。)
app-b
サービス実現のために動かすアプリのリポジトリです。Golangで簡易的なAPIサーバを実装をしています。 (環境変数でDB接続情報等の開発環境/本番環境で変更が必要なパラメータを渡せるようにすることで、同一のDockerイメージで開発環境と本番環境どちらにも対応できるという前提があります。)
ブランチ
各リポジトリのブランチは以下のようになっています。なお、Dockerイメージタグにはバージョン番号ではなくGitコミットハッシュ値を利用しています。
manifests
dev
: 開発環境のアプリの状態を表すブランチ (開発環境・本番環境両方の設定ファイルを含む)prd
: 本番環境のアプリの状態を表すブランチ (開発環境・本番環境両方の設定ファイルを含む)feat/*
: アプリの構成 (テンプレート) を更新するときに手動で作成するブランチ
system-manifests
dev
: 開発環境のミドルウェアの状態を表すブランチ (開発環境・本番環境両方の設定ファイルを含む)prd
: 本番環境のミドルウェアの状態を表すブランチ (開発環境・本番環境両方の設定ファイルを含む)feat/*
: ミドルウェアの構成 (テンプレート) を更新するときに手動で作成するブランチ
cd-manifests
master
: 全環境のデプロイ状態を表すブランチfeat/*
: デプロイ状態を更新するときに手動で作成するブランチ
GitHub-flowで管理されている。
app-a
master
: 最新のapp-aを表すブランチ (各環境のデプロイ状態とは無関係)feat/*
: app-aを更新するときに手動で作成するブランチ
GitHub-flowで管理されている。
app-b
master
: 最新のapp-bを表すブランチ (各環境のデプロイ状態とは無関係)feat/*
: app-bを更新するときに手動で作成するブランチ
GitHub-flowで管理されている。
詳細
アプリリポジトリとmanifestsリポジトリ
CIによる自動的なデプロイ要求への対応
最初にアプリのリポジトリですが、今回はGitHub-flowを前提とし master
ブランチ (とfeatureブランチ) のみとなっています。GitHub-flowで master
ブランチは常にProduction-Readyであることを示しますので、master
が進んだ時点でCIから開発環境を更新するデプロイ要求を自動で行います。
manifestsリポジトリでは、各環境を表すブランチは正にその環境の状態を表しています。つまり、開発環境のデプロイ状態は常に dev
ブランチの状態と、本番環境のデプロイ状態は常に prd
ブランチの状態と等しくなることが期待されています。ここで注意していただきたいのが、dev
ブランチも prd
環境の設定を含んでいる という点です。ディレクトリ構造的には下記のように保持しています。
manifests/ ├ app-a/ # app-aに関するマニフェスト │ ├ templates/ # <- テンプレートファイル │ └ values/ # <- 環境固有設定値 │ ├ dev-app-a.yaml │ └ prd-app-a.yaml └ app-b/ # app-bに関するマニフェスト ├ templates/ # <- テンプレートファイル └ values/ # <- 環境固有設定値 ├ dev-app-b.yaml └ prd-app-b.yaml
manifests/app-a/values at dev · gitops-demo/manifests · GitHub
一見変に感じるかもしれませんが、これは manifestsリポジトリ内の dev
環境の設定と prd
環境の設定は常に同時に更新される ということを暗に要求しています。
分かりづらいと思うのでアプリリポジトリ視点でも考えてみましょう。アプリリポジトリが更新されたとき、まず開発環境に反映するため対応するブランチである dev
ブランチに対してデプロイ要求を行います。
このとき、必ず 開発環境向けの設定だけでなく本番環境向けの設定も更新します。具体例を挙げると、manifestsリポジトリの dev
ブランチに含まれる開発環境向けの設定ファイル (dev-app-a.yaml
) と本番環境向け設定ファイル (prd-app-a.yaml
) に共通するDockerイメージタグを app-a:aaa
から app-a:bbb
に同時に書き換える、というような感じです。PRの差分的には次のようになるはずです。
--- a/dev-app-a.yaml +++ b/dev-app-a.yaml @@ -1,2 +1,2 @@ -image: app-a:aaa +image: app-b:bbb replicas: 1 --- a/prd-app-a.yaml +++ b/prd-app-a.yaml @@ -1,2 +1,2 @@ -image: app-a:aaa +image: app-b:bbb replicas: 10
また、サンプルでいうと以下のCIから生成されたデプロイ要求PRです (コミット内容、コミットハッシュ値等は異なります)。
以下が 実際にその操作を行っているCIのスクリプト の抜粋です。
git clone "https://github.com/gitops-demo/manifests.git" manifests cd manifests git checkout dev # 常にデプロイ要求はdevブランチ (=開発環境) に向ける git checkout -b "ci-build/app-a/${TRAVIS_COMMIT}" # 更新用のブランチを作成 cd app-a/values for f in *-app-a.yaml # 開発環境向け設定ファイル (dev-app-a.yaml) と本番環境向け設定ファイル (prd-app-a.yaml) どちらにも同じ処理を行う do PREV_COMMIT=$(grep -oP "(?<=repository: gitopsdemo/app-a:).+$" "${f}") sed -i -e "s!repository: gitopsdemo/app-a:.\+\$!repository: gitopsdemo/app-a:${TRAVIS_COMMIT}!" "${f}" # Dockerイメージタグを最新のGitコミットハッシュ値に更新 done
manifestsリポジトリの dev
ブランチをデプロイ要求の対象としつつ、開発環境向け設定ファイルと本番環境向け設定ファイルを同時に同じように更新していることがわかると思います。なお、今回のサンプルでは開発環境と本番環境の2つだけですが、例えばステージング環境がある場合はステージング環境向け設定ファイルも同様に処理することになります。
dev
ブランチのみ、本番環境の設定は prd
ブランチのみ持たせた場合も考えてみましょう。簡単なことですが、dev
ブランチと prd
ブランチはそれぞれ独立したコミットログを重ねてしまうためGitの恩恵を受けられません。新しいパラメータを増やそうと思ったら、全く独立した2つのブランチの全く独立した2つのファイルを書き換えることになってしまいます。また、テンプレートも活用できません。
アプリ構成変更を伴う手動デプロイ要求への対応
ここまでアプリごとのDockerイメージを更新するだけで済むケースのみを考えてきました。もう1パターン、manifestsリポジトリ内のアプリ構成を変更するケースを考えてみましょう。サンプルプロジェクトにはありませんが、app-cという第3のアプリの追加を仮定します。
この場合GitOpsではmanifestsリポジトリにapp-c用のマニフェストを追加するため、手動で feat
ブランチを作成する必要があります。feat/add-app-c
は以下の緑色の部分を追加するPRになるはずです。もちろん開発環境だけでなく本番環境の設定も含まれています。
manifests/ ├ app-a/ # app-aに関するマニフェスト │ ├ templates/ # <- テンプレートファイル │ └ values/ # <- 環境固有設定値 │ ├ dev-app-a.yaml │ └ prd-app-a.yaml ├ app-b/ # app-bに関するマニフェスト │ ├ templates/ # <- テンプレートファイル │ └ values/ # <- 環境固有設定値 │ ├ dev-app-b.yaml │ └ prd-app-b.yaml └ app-c/ # app-cに関するマニフェスト ├ templates/ # <- テンプレートファイル └ values/ # <- 環境固有設定値 ├ dev-app-b.yaml └ prd-app-b.yaml
そしてマージ先ですが、これを必ず dev
ブランチにします。これにより、自動化されたデプロイ要求と整合性の取れたブランチ運用を実現します。
開発環境から本番環境への反映
度々になりますが、アプリの変更は必ず最初に dev
環境に対して行うため、CIによるデプロイ要求はmanifestsリポジトリの dev
ブランチに行います。これだけでは本番環境 (prd
ブランチ) が一生更新されませんので、こちらの更新方法についても紹介していきます。方法としては、prd
ブランチに対するCIからの自動的な処理は一切なく、任意のタイミングで手動でmanifestsリポジトリの dev
ブランチを prd
ブランチにマージ します。
サンプルでは以下の手で作成されたPRがこれにあたります。
なぜ自動化されていないのかは2つの理由から成ります。1つ目の理由は、一般的に本番環境は開発環境ほど細かい単位ではデプロイを行わないから、というものです。サービスの更新によっては「複数のアプリの更新を同時に取り込む必要がある」というケースも容易に考えられます。この場合、dev
ブランチに必要な幾つかのアプリが更新が反映された段階で、まとまった更新が prd
ブランチに行われるべきです。2つ目の理由は、一旦開発環境で確認してから本番環境へ上げるというフローの強制がGit的に困難だからで。もしCIフックの度に同じデプロイ要求を dev
ブランチと prd
ブランチに投げてしまうとブランチが独立してしまいます。また、CIによる自動的なデプロイ要求以外に行われるサービス構成の変更もまず dev
ブランチへ行うようにすることで、変更を自然にGit上で伝播させることが可能になります。特殊なケースとして、「dev
ブランチにコミットとして溜まった変更の一部をピックアップして本番環境に反映したい」ということもあるかと思います。この場合はGitのCherry-Pickで自然に解決できます。必要な変更のコミットだけをCherry-Pickで prd
ブランチに拾い上げれば良いのです。dev
ブランチに残った差分をまとめて prd
ブランチに反映させたくなったら dev
ブランチを prd
ブランチにマージするだけです。この辺りはGitを最大限に活用していくことになりますが、運用が煩雑になりすぎないように注意が必要です。
system-manifestsリポジトリ
ここまでmanifestsリポジトリ(=アプリのSingle Source of Truth)についてでしたが、system-manifestsリポジトリについてもフローは全く同じです。(アプリとは異なり、ミドルウェア自体の開発を行うリポジトリは外部にありあくまで利用するだけという仮定のもと、) manifestsリポジトリとは異なり基本的にCIによるデプロイ要求はありません。system-manifestsに対する直接の変更はアプリ構成変更を伴うデプロイ要求と同じように feat
ブランチを作成し、それをまずは dev
ブランチにPR・マージします。そして必要に応じて dev
ブランチを prd
ブランチにマージすることで最終的に変更を本番環境へ反映できます。
サンプルプロジェクト > Gitリポジトリ
でも軽く触れましたがmanifestsとsystem-manifestsはライフサイクルが大きく異なります。もし分けていないと、例えば本番環境のアラート設定のマニフェストを少しだけ変える必要が出た場合、「既に dev
ブランチに溜まっていたアプリの変更差分をprd
ブランチへのマージに巻き込まないようにアラート設定コミットだけをCherry-Pickする」といった煩雑なCherry-Pickが多発して面倒なことになってしまうでしょう。
cd-manifestsリポジトリ
最後にcd-manifestsですが、基本的に master
ブランチのみです。manifestsやsystem-manifestsと比較して、一旦デプロイしてしまえばcd-manifestsのレイヤでの変更 (e.g., アプリ自体の追加・削除等) は少ないはずなので、余計な複雑性を排除するためにこのようにしています。
まとめ
あくまで一例ですが、サンプルプロジェクトを交えながらGitOpsのブランチ戦略を紹介しました。実際に導入するにはどうしてもサービスの要件に合わせたカスタマイズが必要ですが、基本となる部分だけでも、雰囲気だけでも掴んでいただけたのであれば。