ネットワークサービス部の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 では、利点として
- 分散システムに比べて単純であること
- 開発を開始するときには(マイクロサービスと比べ)システム全体を把握することが容易なモジュラーモノリスは生産性が高い
- デプロイが容易であること
- スケーリングが必要になる段階までは、(マイクロサービスと比べ)パフォーマンスが良い
一方欠点として
ことが挙げられています。
🔭 システムの概観
わたしたちのシステムの構造は、下図のように示すことができます。
まず、システムは複数のコンテキスト(モジュラーモノリスでいう「モジュール」。order
や payment
等)から成ります。
コンテキスト間でやりとりをする場合は、各コンテキストが外部に公開している
インタフェース(REST APIやMQ)を使うことになります。
各コンテキストの内部は、以前の記事『【システム開発戦略編 その1】 ドメインドリブンでクリーンなアーキテクチャを意識した開発』でご紹介した通りのクリーンアーキテクチャの考え方に基づく4層の構造になっています。
shared
と書いてあるコンテキストは、実際には「コンテキスト」ではありません。この疑似的なコンテキストは、
各コンテキストの実装にあたり共通で必要な部品を定義しているだけのものです。
それ自身は何らのビジネスロジックを表現せず、したがって通常の意味でのコンテキストではありません。
shared
疑似コンテキストには、例えばデータベースへの接続処理・ユースケースの開始/終了時の処理・
ロギング等、どのコンテキストでも必要になる部品が含まれます。
したがって、各コンテキストは、それ自身と shared
にある共通部品を合わせることで、
他のコンテキストから独立して機能するサブシステムを成します。
なお、コンテキスト間の独立性を保つため、コンテキストをまたいで(Pythonでいう)モジュールをインポートすることは許されません (sharedの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 を 踏まえながら、わたしたちの取り組みをご紹介できればと思っています。お楽しみに。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編
- システム開発戦略編