FJCT Tech blog

富士通クラウドテクノロジーズ公式エンジニアブログです

富士通クラウドテクノロジーズ

FJCT/Tech blog

【システム開発戦略編 その3】モジュラーモノリス

ネットワークサービス部のid:a8544です。

連載22回目の今回は、わたしたちが採用しているアーキテクチャのパターンである モジュラーモノリスについてご紹介します。

以前の記事『【システム開発戦略編 その1】 ドメインドリブンでクリーンなアーキテクチャを意識した開発』で、 DDD・クリーンアーキテクチャの適用例についてご紹介しました。しかし、わたしたちの開発しているシステムのアーキテクチャを特徴づける重要な要素はもう一つあり、 それが今回ご紹介するモジュラーモノリスです。

⛰️ モジュラーモノリスとは

モジュラーモノリスについては、すでに多くの書籍・Webサイト等で解説されています。 そのためここで改めて詳しく解説することはしませんが、Kamil Grzybekのブログ記事 Modular Monolith: A Primer によれば、

  • モノリス はちょうど1つのデプロイ単位で構成されるシステムである
  • モジュラー とは、独立で交換可能な「モジュール」で構成されていることである
  • モジュラーモノリス はモジュラーな方法で設計されたモノリスなシステムである

としています。この定義においては、システムが「モノリス」であるからといって モジュラーでないというわけではありませんし、また「悪い」設計であることを意味するわけではありません。

本記事でもこの定義でこれらの用語を使います。

「モジュール」

「モジュール」は大変多義的な言葉ですが、前掲の記事において、Kamil Grzybekは「モジュール」の要件を

  • ほかのモジュールから独立で、交換可能であること
  • その機能を提供するのに必要な全てのものを具備していること
  • インタフェースが定義されていること

としています。「モジュール」は機能を提供するのに必要なものを全て備えることから、 個々の「モジュール」は以前の記事で ご紹介した「ドメイン層」から「インフラストラクチャー層」までの全ての部品が含まれることになります。

ここでいう「モジュール」は、技術的な機能による分解(GUI「モジュール」、データベース「モジュール」など)ではなく、 ビジネスロジックに着目した分解に基づくものです(注文「モジュール」、支払い「モジュール」など)。 これはDDDにおけるbounded contextに対応します。 そこで、わたしたちはこの「モジュール」のことを「コンテキスト」と呼んでいます。

利点と欠点

ほかのアーキテクチャがそうであるように、モジュラーモノリスにも利点・欠点が存在します。 同じくKamil Grzybekの別のブログ記事 Modular Monolith: Architectural Drivers では、利点として

  • 分散システムに比べて単純であること
    • 開発を開始するときには(マイクロサービスと比べ)システム全体を把握することが容易なモジュラーモノリスは生産性が高い
  • デプロイが容易であること
  • スケーリングが必要になる段階までは、(マイクロサービスと比べ)パフォーマンスが良い

一方欠点として

  • スケーラビリティが高くない(マイクロサービスと比べ)
  • 単一のプロセスで動作するため、障害発生時の影響範囲が大きい
  • ヘテロジーニアス(異なる技術の混在、複数の言語を使って構成する等)な構成がとれない

ことが挙げられています。

🔭 システムの概観

わたしたちのシステムの構造は、下図のように示すことができます。

システムに存在するモジュールが横に並べられ、各モジュールの中でクリーンアーキテクチャのレイヤーが縦に並べられている。
システムを構成するモジュール群

まず、システムは複数のコンテキスト(モジュラーモノリスでいう「モジュール」。orderpayment 等)から成ります。 コンテキスト間でやりとりをする場合は、各コンテキストが外部に公開している インタフェース(REST APIやMQ)を使うことになります。

各コンテキストの内部は、以前の記事『【システム開発戦略編 その1】 ドメインドリブンでクリーンなアーキテクチャを意識した開発』でご紹介した通りのクリーンアーキテクチャの考え方に基づく4層の構造になっています。

shared と書いてあるコンテキストは、実際には「コンテキスト」ではありません。この疑似的なコンテキストは、 各コンテキストの実装にあたり共通で必要な部品を定義しているだけのものです。 それ自身は何らのビジネスロジックを表現せず、したがって通常の意味でのコンテキストではありません。 shared 疑似コンテキストには、例えばデータベースへの接続処理・ユースケースの開始/終了時の処理・ ロギング等、どのコンテキストでも必要になる部品が含まれます。

したがって、各コンテキストは、それ自身と shared にある共通部品を合わせることで、 他のコンテキストから独立して機能するサブシステムを成します。

なお、コンテキスト間の独立性を保つため、コンテキストをまたいで(Pythonでいう)モジュールをインポートすることは許されません (sharedのPythonモジュールを他コンテキストからインポートすることは認められます)。 インポートしてよい(依存してよい)パッケージの関係を示したのが下図です。この図において、 あるパッケージのモジュールは、そのパッケージから出る矢印に従ってたどることのできる パッケージのモジュールのみをインポートできます。

各Pythonモジュールがインポートできるモジュールを示した図。モジュールが2つ存在し、各モジュールにはクリーンアーキテクチャのレイヤー分けがされている。各レイヤーについて、文章中で述べられているルールに基づきインポート可能な場合に矢印が引かれている。
Pythonモジュールがインポートできるモジュール

ディレクトリ構造

参考までに、簡略化したディレクトリ構造(Pythonのパッケージ構造)を下記に示します。 まずシステム全体を含むパッケージ myservice があって、 その下にコンテキストがならんでいます (order, payment, および shared)。 各コンテキストの内部には、 domain, usecase, interface, および infrastructure の4層とDIの定義がそれぞれあって、 各層の内部にコンテキストを実装するPythonモジュールが存在します。 なお、DIについては以前の記事でご紹介しています。

パッケージ myservice.cmd は、このシステムを起動するためのエントリーポイントの定義を含む特殊なパッケージです。

.
├── myservice
│   ├── cmd
│   │   └── entrypoint.py
│   ├── order
│   │   ├── di
│   │   ├── domain
│   │   ├── infrastructure
│   │   ├── interface
│   │   └── usecase
│   ├── payment
│   │   ├── di
│   │   ├── domain
│   │   ├── infrastructure
│   │   ├── interface
│   │   └── usecase
│   └── shared
│        ├── di
│        ├── domain
│        ├── infrastructure
│        ├── interface
│        └── usecase
├── poetry.lock
├── pyproject.toml
└── tests
     ├── large
     ├── medium
     └── small

🚀 統合とデプロイ

各コンテキストでは FastAPIの APIRouterドキュメント)を それぞれ定義しています。このWeb APIは、各コンテキストが有する機能を コンテキスト間、および外部から利用するためのインタフェースを提供します。

複数のコンテキストを1つのモノリスとしてデプロイするために、 エントリーポイントでは FastAPI.include_router() によって各コンテキストの APIRouter を 1つのASGIアプリケーションに統合します。 したがって、エントリーポイント(ASGIアプリケーションの定義)は次のようになります(DIは省略)。

from fastapi import FastAPI

from myservice.order.infrastructure.router import orderRouter
from myservice.payment.infrastructure.router import paymentRouter


app = FastAPI()

v1api = FastAPI()
v1api.include_router(orderRouter, prefix="/order")
v1api.include_router(paymentRouter, prefix="/payment")

app.mount("/v1", v1api)

# Run the system:
# $ uvicorn myservice.cmd.entrypoint:app

それぞれのコンテキストへのリクエストは、

  • http://localhost:8000/v1/order/orders/1a868e13-ea13-49ee-8590-eb00d7f5b467
  • http://localhost:8000/v1/payment/subscriptions

というように、コンテキストごとに異なるプレフィックスのついたパスを使うことになります。

このASGIアプリケーションを起動する1つのDockerイメージをビルドし、それをデプロイすることでシステムが動作します。 コンテキストの個数によらずイメージは1つしかありません。

なお、コンテキストによっては非同期処理を処理するワーカーを別に必要とするものがあり、 これらワーカーは個別のASGIアプリケーションとは別のプロセスとして動作する必要があります。 この場合もDockerイメージをASGIアプリケーションのものと共通にできるよう、 Dockerイメージはコマンドラインオプションで挙動が変わるようになっています。

現在の運用では、単一のDockerイメージをもとに、 ASGIアプリケーションと非同期処理のワーカー群を常に同時に(したがって同一バージョンで)デプロイしています。 これはデプロイ単位を1つとし、管理を容易にするモジュラーモノリスの戦略そのものです。

まとめ

わたしたちのシステムのアーキテクチャを特徴づけている「モジュラーモノリス」についてご紹介しました。

モジュラーモノリスはよくマイクロサービスと比較されます。当然それぞれに利点・欠点が あるため、一概にどちらを選ぶべきとは言えず、そのシステムに応じて種々の要素を考慮しながら 選択が行われることになります。

わたしたちのシステムにおいては、基本的にモジュラーモノリスの考え方を採用し、その利点である 開発環境の構築を含めたデプロイ管理の容易さ・システム全体の構造の理解の容易さを享受しています。 また、開発が始まった当初から見るとビジネス上の要件・要求機能も増大していますが、いまのところ 大きな問題なく規模を拡大できています。

次回は、いよいよ最終回で、 The Twelve-Factor App を 踏まえながら、わたしたちの取り組みをご紹介できればと思っています。お楽しみに。

連載バックナンバー