最大7バージョンの並列開発を支える開発基盤の話

はじめに
株式会社QualiArtsでバックエンドエンジニアをしている岸端です。
QualiArtsでは、現在に至るまでさまざまなプロジェクトの開発を行っています。 各プロジェクトでは数年先までの施策が計画されており、複数の施策を同時に開発することも多いです。
今回は、そんな複数バージョンの同時開発を支える開発基盤について、その仕組みや背景を紹介します。
開発の大まかな流れ
開発基盤の話をする前にQualiArtsでの開発の大まかな流れを紹介します。 プロジェクトによって多少の違いはありますが、以下のような流れで開発が行われます。
- (半年前〜2年前)戦略に基づいて施策を検討
- (3ヶ月前〜9ヶ月前)要件定義
- (1ヶ月前〜6ヶ月前)設計/実装/テストプレイ
- (1ヶ月前〜3ヶ月前)検証/デバッグ
- リリース
施策によっては、要件定義からリリースまでに半年以上の時間がかかることもあります。 プロジェクト内のテストプレイの結果によっては、要件定義や設計を大きく変更することもありますし、ユーザの期待を越えられないと判断された場合は、リリースを見送ることもあります。 プロジェクトによって1つのバージョンのリリース頻度は異なりますが、1〜3ヶ月に1回のリリースを行うことが多いです。 その間、半年〜1年後の施策を同時に開発することも多く、3バージョン以上の同時開発を行なっている場合がほとんどです。 私の経験では、最大で7バージョンの同時開発を行なったこともあります。
開発基盤には、こうした複数バージョンの同時開発によって生じる問題を解決し、開発者がビジネスロジックを含むコアロジックに集中できるようにすることが求められています。
同時に複数バージョンを開発する際の課題
以下のように同時に複数バージョンの実装を行うと、同じタイミングで複数の実装、テスト、リリース準備、バグ修正が発生します。
当然それぞれのバージョンで異なるコード差分が生まれます。 マスターデータの作成、動作確認、QAなどの作業もバージョン毎に対応が必要になります。 任意の上位のバージョンの成果物に対して、下位のバージョンの成果物をマージすることは、バージョンまたぎに起因するバグを早期に発見するためにも頻繁に行う必要があります。バージョン間の差分が大きくなると、成果物のマージコストが増大し、開発者の負担が増加します。 経験則ではありますが、同時に開発するバージョン数の増加に伴い、マージのコストは指数関数的に増大します。
バージョン数が増えると、コミュニケーションコストも無視できないほどに大きくなっていきます。 すべてのバージョ ンの差分を1人が把握することは理想的ですが、現実的には難しい場合が多いです。 バージョン数が増えるということは、関わる開発者の数も増加し、コミュニケーションコストも増大します。
また、開発のイテレーションを早めるためにはリリース頻度を上げる必要がありますが、バージョン間の差分が大きくなりマージコストが増大すると開発者への負担が増え、リリース頻度を上げることが難しくなります。 その環境下でリリース頻度を上げようとすると、多くの時間をマージやリリースの作業に費やすことになり、コアロジックの開発に集中できなくなり生産性が低下します。
バージョン管理のための開発基盤には、こうした課題を解決するための仕組みが求められています。
非エンジニアでも安心してリリースできる仕組み
マージやリリースのように、日々の開発において定常的に発生するオペレーションコストを削減するためには、これらの作業を必要な人が必要なタイミングで安心して行える仕組みが必要です。 私の所属するプロジェクトの開発環境では、エンジニアはいっさい関与せずに、プランナーやクリエイターの方がリリース作業を行なえます。
これを実現する開発基盤を構築するにあたって以下の3つのポイントを意識しています。
- コストの発生を防ぐ
- コストが発生した場合は最小限に抑える
- 別のコストの発生に波及しないようにする
この3つのポイントを軸にして、成果物のマージとリリースのコストを削減するための仕組みを構築しています。
マージコストを削減するには、コンフリクトしにくい成果物の構造と、成果物のマージを自動化する仕組みが必要です。 コンフリクトしにくい成果物の構造を実現することは、コストの最小化にもつながります。 そして、コストの波及を防ぐために、コンフリクトを早期に発見し、解消する仕組みが必要です。
リリースコストを削減するには、エンジニア以外がリリースしても安全にリリースができる堅牢かつ自動化された仕組みが必要です。 堅牢さについては、誤りが発生しにくいシンプルな開発フローと、人間の関与の影響によりエラーが発生しないリリースの仕組みを構築することで担保できます。 開発フローでは、マージの際のコンフリクトが早期に発見され、解消されていることが求められます。
以下で、これらのポイントを実現するための具体的な仕組みについて紹介します。
バージョン番号の定義と管理
エンジニア以外の職種の方がリリースの際に意識するのは、バージョン番号と自分の担当する成果物の構造差分です。 私の所属するプロジェクトでは、バージョン番号を以下のように定義しています。
develop-{リリースバージョン: MAJOR.MINOR.PATCH}.{開発バージョン}-{ビルドバージョン}
セマンティックバージョニングの考え方をベースに前にdevelopプレフィックス、後ろに開発バージョンを付与しています。 開発バージョンは開発中のビルド毎にインクリメントされるバージョン番号です。 4桁目の導入は、リリースバージョンとのコンフリクトを防ぎつつ、明確なリリースバージョンも含めた明確な順序付けを可能にします。
プロジェクトにはAPIサーバやバッチシステム、AIシステムなどが存在します。 この4桁のバージョン番号はこれらのシステム全体で共通した識別子として利用されており、共通の差分を取り込んだ際には同じバージョン番号が付与されます。 開発中のシステムでバグが発覚した場合は、自動化されたリリースの仕組みにより切り戻 しを行うことで迅速な原因の絞り込みと再現を行うことができます。
新しいバージョン番号が切られた際には、その前のバージョンとの差分がSlackに通知されます。 また、管理画面をリリースすれば、現在のバージョンのスキーマ・タイプ定義や、それらに基づくテンプレート出力が可能になります。 これにより、開発者は自分の担当する成果物の差分を把握しやすくなり、リリースの際のコミュニケーションコストを削減できます。
バージョン番号がインクリメントされることのみを理解していれば、どんな職種の方でも最新のバージョンを自分で都合の良いタイミングでリリースできます。
マイグレーションファイルの自動生成と管理
同時に複数のバージョンを開発する場合、データベースのスキーマのマイグレーションが課題となります。 リリース時に安全にマイグレーションを適用するには、順序付けされたマイグレーションの適用が必要です。 一方で開発時には前のバージョンで追加差分が発生することもあるため、順序保証が困難になります。 この理由から開発環境ではマイグレーションによる適用を諦め、データベースのスキーマとの差分を抽出して反映する方式を採用しています。
この方式を採用した場合の問題は、本番のマイグレーションの適用の際に発生する問題を発見する契機を失うことです。 データベースのスキーマを安全に変更することはエンジニアの責務です。 この問題は、マイグレーションファイルの自動生成、検証、レビューを開発フローに組み込むことで解決しています。 「別のコストの発生に波及しないようにする」という考え方に基づき、検証タイミングを前倒してCI/CDの際に担保できるようにしています。 具体的なフローは以下の通りです。
- 開発者はコード上の自動生成元となる定義ファイル(Protocol Buffers)を更新する
- 開発者は自動生成スクリプトを実行し、コード全体の依存箇所を更新する
- 開発者はプルリクエストを作成し、レビューを受けた後にマージする
- マージ後、CI/CDパイプラインは自動的に本番用のマイグレーションファイルを生成する
- CI/CDパイプラインはマイグレーションの適用を検証するためにコンテナ化されたDBにマイグレーションを適用して検証する
- CI/CDパイプラインはマイグレーションの適用が成功した場合に生成されたマイグレーションファイルに関するプルリクエストを作成する
- 開発者はレビューを行い、問題が なければマージする
マイグレーションの自動生成と検証をCI/CDパイプラインに組み込むことで、マイグレーションの適用が安全であることを保証できます。 本番環境やステージング環境ではこの順序以外の順序でマイグレーションは発生しません。
マイグレーションファイルの命名規則は以下の通りです。
{リリースバージョン: MAJOR,MINOR,PATCHのそれぞれ3桁の数字}{マイグレーション生成日時: YYMMDDHHMM}.sql
マイグレーションの生成日時を含めているのは、ステージング環境でバージョンの途中でマイグレーションを適用する場合に適用順序を明確にするためです。
Slackからのリリース
エンジニア以外の職種の方でもリリースを行えるようにするために、Slackからのリリースを可能にしています。 Slackのコマンドを使用して、対象の開発環境とリリースバージョンを指定することで、リリースを行うことができます。 リリースを開始するとSlackにリリースの進捗が通知され、20秒ほどでリリースが完了します。
Slackコマンドによるリリースは以下のような構成になっています。
リリースコマンドでは、DBのスキーマ状態の同期、コンピュートのインスタンスの起動と切り替えが行われます。 すべての開発環境のリリースはこのコマンドを使用して行われ、同じSlackのチャンネルに通知されます。 これにより、誰でもリリースを行えることと、リリースをチーム全体で把握することの両立が可能になります。
実際の効果
開発フローを整備した効果を実際の私が所属するプロジェクトのデータをもとにして考えてみましょう。 まず、過去1年間の開発環境へのリリース回数は約1500回でした。 そして、エンジニアが1回あたり10分の時間をかけてリリースを行うと仮定すると、年間で約250時間(約10営業日)のオペレーションコストが発生します。 また、マイグレーションにも10分の時間をかけていると仮定すると、年間でさらに約250時間のオペレーションコストが発生します。 つまり、年間で約500時間のオペレーションコストが発生していることになります。
開発フローを整備したことでエンジニアがこれらに関与することはなくなり、プランナーやクリエイターは待ち時間なくリリースを行うことができるようになりました。 エンジニアはオペレーションコストが削減され、コアロジックの開発に集中できるようになりました。
最後に
開発フローを整備し、開発基盤を構築することで、複数のバージョンの同時開発に伴うオペレーションコストを削減し、開発者がコアロジックに集中できる環境を整えることができました。 単なる時間的なコストの削減だけでなく、開発者の思考のコンテキストスイッチを減らすこともでき、生産性の向上にもつながりました。 このような開発基盤の整備はプロジェクトの規模が大きくなるほど重要になっていくと考えています。
この記事が同様の課題を抱えるプロジェクトの開発者の方々にとって参考になれば幸いです。