Rails モノリスの分解
| Status | Authors | Coach | DRIs | Owning Stage | Created |
|---|---|---|---|---|---|
| proposed | grzesiek
fabiopitino
ayufan | 2023-05-22 |
概要
メインの GitLab Rails プロジェクトは、Ruby on Rails フレームワークを使用する大規模なモノリシックアプリケーションとして実装されています。220 万行以上の Ruby コードを持ち、何百人ものエンジニアが毎日コントリビュートしています。
このアプリケーションは 10 年以上にわたって複雑さが増しています。この間、モノリシックアーキテクチャは高い開発速度と優れたエンジニアリング生産性を維持することを可能にし、私たちに大きく貢献してきました。
アプローチしやすいオープンコアアーキテクチャを目指しつつも、速度を維持し開発の予測可能性を高めるために、ドメイン間の境界を強化する必要があります。
フロントエンドにも同じことが言えます。Web UI は、大規模で密結合な JavaScript と Vue のコードベースであり、単一の Webpack バンドルとしてビルドされ出荷されています。これはモノリスと同じ運命を共有しています。すべてが一緒にコンパイル、デプロイ、リリースされ、UI の一領域だけを他と独立して進化させたり出荷したりする方法がありません。そのため、分解には 2 つの補完的なトラックがあります。Rails アプリケーションをモジュール化する バックエンド トラックと、ビルドパイプラインと UI をモジュール化する フロントエンド トラックです。どちらも以下で説明する同じ目標と原則を共有します。
なぜ今か: エージェント型への必然性
ソフトウェアの作られ方は変化しています。エージェント型 AI によって、Issue が作成されるとすぐにエージェントが作業を引き受け、コードベースの明確に定義された領域内で独立して動作し、MR を開く前に自分の作業を検証できるようになると、可能なことが変わります。これを実現するには、モジュールに明確な境界、明示的な契約、そしてビルドや実行方法に組み込まれた暗黙知がないことが必要です。
モノリスは、何年ものコンテキストを頭の中に持つ人間の開発者向けに作られました。エージェントにはそのようなコンテキストがありません。依存関係が暗黙的で、安全に作業するために必要な知識がすべてリポジトリ内に存在しているわけではないため、全体を推論せずに 1 つの領域を安全に変更することはできません。モジュール境界は、コードベースのある領域をエージェント、そして人間が単独で推論できるほど小さくします。
これはモジュール化の位置づけを変えます。もはやエンジニアリング速度や認知負荷だけの話ではありません。大規模なエージェント型開発のための構造的な前提条件です。明確な契約、自己完結したコンテキスト、高いテストカバレッジを持つモジュールは、エージェントが自律的に作業でき、修正がより早くお客様に届き、チームがモノリスの結合に引きずられず自分たちのペースで進める場所になります。
目指しているもの
私たちは、サテライトサービスを持つモノリシックアーキテクチャを引き続き使用しながら、モジュラーモノリスデザインへ進化させたいと考えています。また、投資に見合うモジュールについては、GitLab.com、Dedicated、セルフマネージドインスタンス上で、単一の契約に対してモノリスと統合される、独立してデプロイ可能な単位へ進めたいと考えています。
これにより、エンジニアリング効率を高め、認知負荷を軽減し、コードベースを AI エージェントが読み解けるものにし、最終的には投資対効果が明確な場合に別々にデプロイ・実行できる程度まで内部コンポーネントを切り離せるようになるはずです。
フロントエンドでは、これは独立してビルド・デプロイ可能な UI モジュールが、共有シェルアプリケーションと明示的なモジュール契約を通じて統合されることを意味します。これは、バックエンドのバウンデッドコンテキストとパブリックインターフェイスに対応する視覚的な対応物です。
動機
大規模で密結合なモノリシックアプリケーションを扱うことは困難です:
エンジニアリング:
- エンジニアのオンボーディングに時間がかかります。コンテキストの規模と結合の量により、エンジニアが生産的だと感じるまでに時間がかかります。
- いくつかのドメインに
CODEOWNERSファイル機能を使用する必要がありますが、これらのルールは複雑です。 - アプリケーションの規模が大きいため、エンジニアがアプリケーションのメンタルマップを構築することは困難です。一見分離されている変更でさえ、モノリスの他の部分に広範な影響を及ぼす可能性があります。
- エンジニアリング人材の離職と定着。生産性への障壁と絶えず向き合うことは、エンジニアにとって疲弊し士気を下げるものです。
アーキテクチャ:
- モノリスの内部にはほとんど構造がありません。いくつかのモジュールの作成を強制しようとしてきましたが、モノリスの機能的な部分がどうあるべきか、コードをどのように整理すべきかについて、全社的な戦略はありません。
- 既存のモジュール間に分離がありません。Ruby は境界を効果的に強制するための組み込みツールを提供していません。すべてが同じメモリ空間の下に存在します。
- 効率を高める抽象化を構築することはほとんどありません。
- 境界はデータベースについて何も語っていません。一見分離されているドメインでさえテーブルを共有し、境界をまたぐ外部キーを持ち、境界線を越える ORM 関連を維持しています。抽出を実際に妨げるのはデータ結合です。
- 高い結合のため、アプリケーションの安定した部分を別サービスに移すことができません。
- 特定ドメインへの変更を個別にデプロイし、その中で発生している障害を分離することができません。
エージェント型 AI:
- エージェントには暗黙のコンテキストがありません。依存関係が暗黙的で変更の影響範囲が不明確なため、全体を推論せずに 1 つの領域を安全に変更することはできません。
- ドメインで作業するために必要な知識、つまり境界、契約、不変条件、runbook が、すべてリポジトリ内に存在しているわけではありません。
- 明確な境界と自己完結したコンテキストがなければ、エージェントは人間が暗黙知を補わずに Issue を引き受け、変更を行い、検証し、MR を開くことができません。
顧客価値:
- セルフマネージドおよび Dedicated のお客様は、アップグレードするまで修正を見ることができず、それが数か月後になることもあります。モノリス全体のリリースを出荷せずに、単一の機能領域に対して対象を絞った修正をロールアウトすることはできません。
- 1 つのドメインの障害がアプリケーション全体を劣化させる可能性があり、負荷の高いドメインを他から独立してスケールさせることもできません。独立したモジュールは別々にスケールし分離できるため、信頼性とフォールトトレランスが向上します。
生産性:
- 複雑な変更の本番環境到達までの中央値が長い。
- より広いコミュニティメンバーにとって、コントリビュートは圧倒されるものになる場合があります。
- テスト時間の削減には、勤勉で粘り強い取り組みが必要です。
目標
- 関心の分離により、開発速度と予測可能性を高める。
- 結合を減らし有用な抽象化を導入することで、コード品質を改善する。
- モジュールをエージェント対応にする。明確な境界、明示的な契約、そしてエージェントが自律的に作業するために必要なすべてのコンテキストをリポジトリにコミットする。
- 依存関係を明示的にし、強制する。宣言されていない依存関係は存在しません。裏口から結合を再作成することはできません。
- 広くモジュール化し、選択的に抽出する。ほとんどのコードはモノリス内でそのままモジュール化されます。ROI が明確な場合にのみ、モジュールを別々にデプロイされるサービスへ抽出します。
- 抽出が正当化される場合に、そのコストを安くする抽象化を構築する。データ所有、独立した運用性、独立したデプロイにより、修正がモノリス全体のリリースなしでお客様に届くようにします。
バックエンド分解
バックエンドトラックでは、GitLab Rails アプリケーションを、明示的かつ強制された境界を持つバウンデッドコンテキストへモジュール化し、独立して運用可能で、ROI が明確な場合には独立してデプロイ可能なドメインを目指します。
どのように実現するか?
モジュール化が重要な技術的な取り組みであることは認識していますが、主な課題は技術的なものではなく組織的なものだと考えています。モジュールが実用的な方法で切り離されるように分離を設計するだけでなく、GitLab での働き方に沿ってモジュール化を整合させる必要があります。
モノリスのモジュール化を成功させるためには、多くの側面と詳細が必要です。以下にリストされた側面に取り組み、それらを洗練させ、目標に向けて進みながらさらに重要な詳細を追加していきます:
- 主要な洞察をもたらすモジュール化のプルーフオブコンセプトを提供する。
- バウンデッドコンテキストを定義することにより、モジュール化計画をプロダクト構造に整合させる。
- 横断的ライブラリを gem に抽出する。
lib//ee/libからプラットフォームコードを取り出します。価値は抽出パターン、ツール、強制された分離にあり、CI 時間の削減ではありません。 - トランスポート層をアダプターへ分解する。Web、REST、GraphQL、Sidekiq の各トランスポートを分離し、それぞれがドメイン層のみに依存するようにします。これにより、API のみ、または Sidekiq のみのノードのようなランタイムプロファイルが可能になります。
- ドメイン層を分離することで、各ドメインが自身のデータを所有し、パブリック API を通じてのみ到達されるようにする。
- 切り離されたドメインを扱う方法について、チームメンバー向けのトレーニングプログラムを開始する(TODO)
- 制御の反転によって切り離されたドメインを構築しやすくするツールを構築する(TODO)
- モノリス内に六角形アーキテクチャを導入する
- 一方向依存関係とホストアプリケーションによるクリーンアーキテクチャを導入する(TODO)
- ドメインを別々に実行・デプロイできるようにする抽象化を構築する(TODO)
決定
- ADR-001: アプリケーションドメインのモジュール化? アプリケーションドメインとインフラストラクチャコードのモジュール化から始める。
- ADR-002: 機能カテゴリを中心にバウンデッドコンテキストを定義する。コード内の SSoT として。
- ADR-003: すべてのモジュールとライブラリにスチュワードを割り当てる。
- ADR-004: 横断的ライブラリを gem に抽出する。
用語集
modulesは Ruby モジュールであり、コードを階層的にネストするために使用できます。namespacesは Ruby 定数の一意の階層です。例えば、Ci::だけでなくCi::JobArtifacts::やCi::Pipeline::Chain::も含まれます。packagesは関連する機能をグループ化するための Packwerk パッケージです。これらのパッケージはデザインとアーキテクチャによって大きくも小さくもなれます。パッケージ内のすべての定数(クラスとモジュール)は同じ名前空間を持ちます。例えば:ciパッケージでは、すべてのクラスはCi::名前空間の下にネストされます。Ci::PipelineProcessing::のようなネストされた名前空間もありえます。ci-pipeline_creationパッケージでは、すべてのクラスはCi::PipelineCreation::Chain::CommandのようにCi::PipelineCreationの下にネストされます。ciパッケージでは、パッケージの名前空間と一致しないため、MergeRequests::UpdateHeadPipelineServiceという名前のクラスは許可されません。- これは Packwerk ベースの RuboCop Cops で簡単に強制できます。
bounded contextは、ドメインのマクロな側面を表すトップレベルの Packwerk パッケージです。例えば:Ci::、MergeRequests::、Packages::など。- バウンデッドコンテキストは単一の Ruby モジュール/名前空間で表されます。例えば、
Ci::JobArtifacts::ではなくCi::です。 - バウンデッドコンテキストは 1 つまたは複数の Packwerk パッケージで構成できます。ドメインが非常に複雑で、実装詳細全体にプライバシーを強制したい場合は、ネストされたパッケージが推奨されます。例えば、
Ci::PipelineProcessing::とCi::PipelineCreation::は同じバウンデッドコンテキストの別々のパッケージにでき、実装詳細をプライベートに保ちながらパブリック API を公開できます。 RemoteDevelopment::のような新しいバウンデッドコンテキストは単一のパッケージで表すことができますが、Ci::のような大規模で複雑なバウンデッドコンテキストは、より小さな/ネストされたパッケージに編成する必要があります。
- バウンデッドコンテキストは単一の Ruby モジュール/名前空間で表されます。例えば、
フロントエンド分解
目標は、独立してビルド可能な UI モジュールが共有シェルアプリケーションと明示的なモジュール契約を通じて統合される、モジュラーなフロントエンドです。これはバックエンドのバウンデッドコンテキストとパブリックインターフェイスを反映します。
フロントエンドのワークストリームは次のとおりです:
- ビルドパイプラインをモダナイズする。独立したフロントエンドモジュールのデプロイを可能にします。
- Webpack 4 から Webpack 5 / Vite へ移行する。また、これによって可能になる Vue 3 移行を完了します。
- モジュール契約を定義する。モジュール境界をまたいだ認可、ユーザーコンテキスト、フィーチャーフラグを管理します。
- 支援インフラストラクチャを構築する。シェルアプリケーション、モジュールレジストリ、Context provider API です。
- “Frontend LabKit” を作成する。共有ライブラリを標準化し、モジュール間で一貫したユーザー体験を確保します。
詳細な設計については、フロントエンド分解 を参照してください。
参考資料
ドメイン層を分離する
トランスポート層をアダプターへ分解する
バウンデッドコンテキストの定義
横断的ライブラリを gem に抽出する
六角形 Rails モノリス
バックエンド分解 ADR 004: 横断的ライブラリを gem に抽出する
モジュラーモノリス ADR 001: アプリケーションドメインのモジュール化
モジュラーモノリス ADR 003: モジュールのスチュワードシップ
モジュラーモノリス ADR 002: 境界付けられたコンテキストの定義
参考資料
ドメインモジュールをパッケージに変換する
モジュラーモノリス: PoC
c955a93f)