最近の開発では、CI/CD、自動テスト、継続的テストが当たり前となっていますが、その影響で、それらのCI/CD方針、テスト方針と、Git等のバージョン管理のブランチ方針をどう連携させるかが、定番の課題になっていると感じています。
今回は、このブランチ方針、CI/CD方針、テスト方針を連携させて、開発の品質とスピードを向上させるアプローチについて解説します。
結論から言うと、要点は以下の二つとなります。
- バージョン管理のブランチ方針は、CI/CD方針、テスト・QA方針と不可分であり、連携を考えながら方針立てする必要がある
- ブランチ方針の工夫で、CI/CD、テスト・QAの開発インフラリソース消費を削減でき、本当に重要なポイントに開発インフラリソースを投入できる。これにより、限られたリソースでの高品質・高スピードの両立を支えられる
背景:開発インフラの進化が全てを解決すると楽観視していた発展期
ここ十数年の間、ソフトウェアエンジニアリングの分野で、明確に進化したといえる分野の一つが開発インフラです。特にCI/CD、デプロイメントパイプラインについての進化は目覚ましく、次のような変化がありました。
- AWSやAzureのようなIaaSが普及し、開発インフラリソースの柔軟なスケールアウトや大規模運用が容易になった
- Docker等のコンテナ仮想化が開発インフラで当たり前になり、開発インフラのランナーやエージェントのセットアップ・運用が容易になった
- k8sのようなコンテナオーケストレーションのツール・サービスの充実で、動的なランナーやエージェントのライフサイクル管理が容易になった
- デプロイメントパイプラインの記述が高度化した。かつてはJenkins1デフォルトのGUIベースのコンフィグ設定のような定義形式が多かったが、Github ActionsやJenkins2 Pipelineのような高度なスクリプトベース・テキストベースのパイプライン定義が可能になった。また事前に使用インフラやブランチを静的定義するのではなく、パイプライン実行時に動的にブランチやインフラを選択するパイプライン記述が容易になった
- End to Endテストやヘッドレスの自動化ツールが充実し、CI/CDに様々なテストやQAの活動を統合可能になった
他にも様々な進化がありましたが、こうした進化を目のあたりにして、この先に「全ブランチで、継続的かつフルセットのテスト・QAタスクを実行して、常に高品質状態を維持」「時間がかかるパイプライン処理は、求める時間になるまで分割・並行処理して時間短縮し、常に実行」ができる理想のCI/CD時代が来る、と思っていた時期が昔ありました。
開発インフラリソースに制約のある現状
ただ、現時点では、これら理想の実現にはいろいろ制約が残っている状態です。例えば次のような制約が残っています。
- IaaSやコンテナオーケストレーションは開発インフラサーバのスケーラビリティや保守性を劇的に改善したが、思ったほど安くなかった。特に中規模以上のコストのスケールメリットが小さかった。筆者の経験として、例えばCI/CD・自動テスト向けの50台ぐらいの物理PCインフラをIaaSに移行させた際、保守性が劇的にあがり、Flaky Testが大幅削減されて移行効果はあったものの、運用コストの大きさから必要なサーバリソースの最大量を常時保有する状態にできなかった
- インフラリソースの増大に連動して、リソース消費量も増大した。例えばEnd to Endテスト、APIテストなどリッチな自動テストが増えた。またコンテナ仮想化は、VMに比べれば緩和しているものの、メモリや時間のリソース消費が大きく、並行化によるリソース削減効果に制約が出た
しかし、ソフトウェア開発では高品質と高スピードの両立の要求が高まっていて、それを支える開発インフラリソースの需要が増えています。
というのも、より有効で詳細な自動テストを実現すればそれに相応する品質確認ができますし、たくさんの開発インフラリソースを投入すれば、デプロイメントパイプラインを高速化できるためです(リソースのスケールアップで処理速度を高めたり、スケールアウトで並行化を促進したりするなど)。そのため使えるリソースに合わせて使用量が飽和している状態が続いています。
この、開発インフラリソースが不十分であることと、需要が増大していることの板挟みの結果、現代的な開発の多くでは、少ない開発インフラリソースを、うまくやり取りするリソース最適化が求められるようになりました。具体的には、詳細なテスト・QAが必要なポイント、時間制約が厳しいポイントに、リソースを厚く割り当て、それ以外ではリソースを浅く割り当てる、という方針が必要になりました。
CI/CD方針、テスト方針と連動させたブランチ管理方針
この「開発インフラリソースの割り当ての最適化」の有効手段の一つに、コードのブランチ管理と、CI/CD方針、テスト・QA方針を連携させて、ブランチごとに開発インフラリソースの割り当てを優先付けするアプローチがあります。
具体的には、開発インフラリソース需要とCI/CDのタスク密度を基準に、以下のようにブランチを三分類します。
- 変更を受容しながら、継続的に高品質を維持するブランチカテゴリ
- 変更を限定しながら、継続的に高品質を維持するブランチカテゴリ
- 変更を受容しながら、開発の都合に柔軟に対応するブランチカテゴリ
- 開発インフラリソース:小、テスト・QAタスク密度:薄
- 軽量なテスト・QAタスクを必要に応じて実行
そして様々な工夫で、メインブランチカテゴリ、保守ブランチカテゴリのブランチ数を絞ることで、限られた開発インフラリソースで、高品質・高スピードの両立を実現するアプローチとなります。
なお、このアプローチは珍しいものではありません。今流行のトランクベース開発や、筆者が好きなGitlab-flowでも、このようなカテゴリ分けに基づく、開発インフラリソースの割り当て最適化がよく行われています。
メインブランチ、リリースブランチ、開発ブランチの三分類で開発インフラリソース割り当てを最適化する
前述のブランチ分類法を、より実例の方針に具体化したものを示します。ここでは、メインブランチ、リリースブランチ、開発ブランチの三分類でブランチ方針を具体化します。
●メインブランチカテゴリ
(前述の「変更を受容しながら、継続的に高品質を維持するブランチ」カテゴリ)
【概要】 チームの開発の中心となるブランチ。フルスペックのテスト・QAタスクを投入し、CiCDのインフラリソースも最大限投入して、高品質な品質を常に維持する。
【ブランチの役割】 開発中のコードの最新の正式版を管理する。プロダクトすべてのコードをチーム全体に共有するために用いる。トランクベース開発でのトランクに該当する。
【ブランチの品質要求】 詳細なテスト・QAタスクをフルスペックで継続的(高頻度に)に実行させ、それらがグリーンであることを保つ。パイプラインに障害が発生しても、即時にCI/CDで検出し、高速に修正して、継続的にコードが高品質な状態を維持する。
【変更の受容】 積極的な変更の受け入れを許容する。高頻度の変更の受容と高品質の両立は、CI/CDのテスト・QAタスクで支える。
【開発インフラリソースの割り当ての考え方】 リソースを最優先で投入する。
●開発ブランチカテゴリ
(前述の「変更を受容し、開発の都合に柔軟に対応するブランチ」カテゴリ)
【概要】 一般的なフィーチャブランチである。各開発者の作業をさせるために、開発者の都合に合わせて自由に運用する。
【ブランチ方針】 メインブランチや開発ブランチからブランチを取る。
【ブランチの役割】 開発の各種作業用に用いる。開発者の都合に応じて自由に作成する。
【ブランチの品質要求】 各チームに合わせる。ユニットテストといった常識的なテストタスクをグリーンに保つことは行うが、詳細なEnd to Endテスト等、リソース負荷の高いタスクは、リソースに余裕がないならばスキップしたり、実行頻度を落としたりする。
【変更の受容】 各チームに合わせる。開発者の都合に合わせて自由に変更する。
【開発インフラリソースの割り当ての考え方】 優先度を低くする。後述するCI/CDや設計・インテグレーションの工夫で、少ないリソースでも品質を確保できるようにする。
●保守ブランチカテゴリ
(前述の「変更を限定しながら、継続的に高品質を維持するブランチ」カテゴリ)
【概要】 リリース等のために、品質が確保されたコードを分離管理する場合に利用する。Gitlab-flowのリリースブランチが該当する。
【ブランチ方針】 通常はメインブランチからブランチを切る。
【ブランチの役割】 メインブランチ・開発ブランチの頻繁な変更から分離して、コードを高品質な状態で保守するために用いる。主用途はリリースコードの保守である。
【ブランチの品質要求】 メインブランチと同等の品質を維持する。ただし、ブランチに関わる品質リスク(マージ、チェリーピック等に起因するリスク)を抑える対策で、テスト・QAタスクを簡略化する方針をとる。
【変更の受容】 変更はなるべく許容しない。
【開発インフラリソースの割り当ての考え方】 優先度を低くする。テスト・QAタスクの実行頻度や実行時間の制約を緩和して、リソース消費量を減らす。
高品質と高スピードの開発を支えるブランチ管理のプラクティス
この三分類を使ったブランチ管理方針により開発インフラリソース割り当ての最適化を進める中で、次のような設計やインテグレーション、テスト・QAのプラクティスを推進すると、高品質・高スピードの両立を促進できます。
設計とインテグレーションの工夫でメインブランチカテゴリを最少化する
メインブランチカテゴリは開発インフラリソースを大量に使用するため、そのブランチ数は最小化すべきです(モノリシックリポジトリなら理想は一つ)。
反面教師として、多くはありませんが、コードのバリエーションごとにブランチを分ける運用方針をたまに見ます。例えば「本番用コードとデバッグ・テスト用コード(本番環境依存部をTest Doubleに置換したものなど)でブランチを分ける」「機能のあるなし・機能の違いでブランチを分ける」「仕向け(日本語向けと英語向け等)でブランチを分ける」といったものです。
これは悪いブランチの運用です。あるべき姿としては、一つのブランチですべてのバリエーションを全部入りで管理し、設計、インテグレーション、動的な切り替え手段で、取捨選択すべきです。切り替え手段には、例えば以下のような仕組みがあります。
- プリプロセス、ビルド、インテグレーション、デプロイのスクリプト(例えばBazelやMakefileでバリエーションを選択)で切り替え
- フィーチャトグルやDependency Lookupといった、機能の有効無効を制御する手段で切り替え
- デバッグモード機能といった、ソフトウェア実行中の設定操作での切り替え
CI/CDに結合した継続的テストを充実させて、保守ブランチカテゴリを最少化する
保守ブランチカテゴリは、リグレッションテストが充実しているほど、必要性が下がります。理想状態ですが、例えばリリース判断できるほど十分なテストが自動化されCI/CDに組み込まれていれば、リリース管理は、メインブランチカテゴリへのタグ付けで実現でき、リリースブランチは不要になります。
GoogleのDORAの調査でも、最上位の技術的組織では、その理想状態の実現により、リリースブランチは事実上存在しないと報告しています。
ですのでCI/CDに組み込んだ自動テストを一定レベル以上に充実させると、保守ブランチカテゴリのブランチが減り、メインブランチにより多くの開発インフラリソースを投入できるようになります。
テスト用にブランチを切るのを避け、メインブランチに対して継続的にテストを行い、メインブランチの品質を育てる
システムテストのような手動の大規模なテストをする際、テストと並行する開発作業によるリグレッションやバグ混入から守るため、テスト用にブランチを切って、そこに対してテストする運用は珍しくありません。
ただこの運用は次の問題を持ちます。
- テスト用ブランチのために開発インフラリソースの使用量を増やします。
- デバッグ時にブランチ間のチェリーピックや複雑なマージが必要となり、それら起因の品質リスクを高めます。
- テストで用いるコードと、開発している最新コードの差異が広がります。それによりテスト結果が陳腐化しますし、差異からのバグ見逃しの可能性も高まるため、開発コードを再度テストしなければならなくなります。
上記の問題から、広い視点で見ると、テスト用にブランチを分けないパターンよりも品質リスクが悪化する恐れがあります。
そのため、品質とスピードの両立では、テスト用にブランチを切るのではなく、継続的テストのアプローチをとった方が都合が良い場合が結構あります。例えば大規模なシステムテストを行うならば、ユースケース単位などでテストを細切れにして、メインブランチに対して細かく・継続的にテストを加えていくアプローチです。
クオリティーゲートを重厚化するのではなく、開発・修正のリードタイムを高速化して品質維持し、ブランチのライフタイムを短縮する
開発ブランチカテゴリ、保守ブランチカテゴリは、ブランチのライフタイムが長いほど、メインブランチとの差分が広がり、ブランチ関連作業の品質リスクを高めます。これはブランチの開発インフラリソース消費の増大につながります。
そのため、開発ブランチや保守ブランチは、なるべくライフタイムが短くなるように運用するのが望ましいです。
対策としては、チームの開発力を高めて、メインブランチの修正のリードタイムを高速化することで、ブランチのマージを許容するアプローチがあります。壊れないようにクオリティゲートを重厚化するのではなく、壊れたらすぐ治すようにチームを鍛えるということです。また、同じくチームの開発力を高めて、開発ブランチの目的達成を高速化することでも、ブランチのライフタイムの削減につながります。
設計の工夫で、ブランチ作業に起因する品質リスクを抑える
ブランチ、特にそれぞれの開発ブランチの成果物をメインブランチに集めるインテグレーションにかかわる品質リスクを抑えると、開発ブランチカテゴリへの開発インフラリソースを削減できます。
例えばマイクロサービスアーキテクチャの推進など、アーキテクチャレベルで結合性を下げる設計を行うと、各コンポーネントのインテグレーションに起因する品質リスクを下げられます。これにより、開発ブランチでは、各コンポーネントのユニットテスト・結合テストのみ行い、大規模なシステムテストはメインブランチに対してのみ適用するといったアプローチが可能となり、開発インフラリソースの割り当て最適化を促進できます。