kubell Creator's Note

株式会社kubellのエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

モノリスからマイクロサービスへの過渡期をソフトウェアで構造化する

こんにちは、ファサード開発チームの佐藤です。
現在 kubell では、 Chatwork のバックエンドを、長年積み上がったモノリスから、ドメインごとのマイクロサービス群へと少しずつ切り替えています。 その「玄関」にあたるのが、Go 言語製 GraphQL API である Facade API です。 本記事では、移行過渡期における Facade API のアプリケーションパッケージ戦略を紹介します。

1. はじめに: 移行過渡期における Facade API

私たちは Chatwork のバックエンドを、長年積み上がったモノリスから、ドメインごとのマイクロサービス群へと少しずつ切り替えています。その「玄関」にあたるのが、API ゲートウェイである Facade API です。Facade API は GraphQL サーバーで、Go 言語で書かれています。

FacadeAPIが未リリースの状態。クライアント・サーバー間の IF 負債と、 サーバーアプリケーション・データベースモノリスが課題

ToBe: アグリゲーション層(図中 Facade API)を導入し、 IF負債・サーバーサイドのモノリスを解消する

我々の目標は、モノリスアプリケーションの退役をなるべく早く実現することです。このため、移行期間においてはマイクロサービスを作らない戦略を取っています。代わりに、Facade API が分割後のドメインロジックを一時的に持つことを許容します。

アグリゲーション層である Facade API は本来、各マイクロサービスが提供する API を束ねるだけの薄い層であるべきです。しかし Facade API は、過渡期においてマイクロサービスの責務を一時的に肩代わりしていることになります。これは紛れもない技術的負債ですが、移行を素早く進める以上避けられない現実でもあります。

ここでいう技術的負債には二つの種類があります。一つ目は、マイクロサービスがまだ存在しないため、本来そちらが持つべきドメインロジックを Facade API が肩代わりしているものです。二つ目は、マイクロサービスは存在するものの GraphQL を喋らないため、Facade API がインターフェイスの変換を担っているものです。本記事で主に扱うのは一つ目の負債です。

移行過渡期のアーキテクチャ図。マイクロサービスが持つべきロジックを一時的に Facade API に載せる

問題は「その負債をどこに、どう置いておくか」です。本記事では、私たちがこの問いに対して出した答えとして、 microservices/ という名のディレクトリを切る、という構造的アプローチについて書きます。

microservices/ 配下のコードは、将来マイクロサービスが担うはずのドメインロジックを、Facade API のリポジトリの中で独立した「サービスのようなもの」として振る舞わせます。呼び出す側の Facade API からは、あたかも外部のサービスを呼んでいるかのように見えます。このおかげで、本物のマイクロサービスが建ったときには呼び出し先を差し替えるだけで移行できます。

2. 専用のパッケージを切らないという選択肢

まず、最も簡単な選択肢として、マイクロサービスパッケージをわざわざ切らないという案を考えてみます。これは短期的には一番楽です。既存の controller、repository、middleware と同じ場所に置けば、import も自由ですし、共通のユーティリティもそのまま使えます。

ですが「いつかこれを別サービスへ剥がす」という未来を前提に置くと、この方針は途端に苦しくなります。

  • Facade API 内のパッケージ、設定値が肥大化します。
  • レビュー時に「これは Facade API 固有のロジックか、それともマイクロサービス的責務か」の境界が見えにくく、コードの見通しが悪くなります
  • マイクロサービス化のタイミングで「どこからどこまでを持っていけば良いのか」を全件人力で判定することになります

負債を負債として認識できないコードは、最も返済しづらい負債になってしまいます。

初めからマイクロサービスを生やすことは難しいのか?

初めからマイクロサービスを生やすことが理想形ではありますが、それには「マイクロサービス基盤の整備」が必要であり、デプロイパイプライン、認証、監視、SLO、オンコール体制まで含めて作り込まなければなりません。

そのため、この登り方は今は採れないという判断になりました。

3. 我々の戦略:目立つ microservices パッケージを切る

そこで私たちが取ったのが、Facade API のリポジトリ内に microservices/ というトップレベルディレクトリを新設するアプローチです。

microservices/
└── <domain>/              
    ├── read/              # Read 系処理をまとめたパッケージ
    │   ├── service.go     #  公開 interface
    │   ├── model.go       #  公開 DTO
    │   ├── spec/          #  ユースケース仕様 (Markdown)
    │   ├── factory/       #  配線 (DI)
    │   └── internal/      #  非公開ロジック(ドメインロジック・リポジトリへのアクセス方法)
    └── write/             # Write 系処理をまとめたパッケージ(内部構成は Read と同様)

「ここに置かれているコードは、いずれマイクロサービスへ移管される」という意図を、ディレクトリ名そのもので表明しています。

併せて、パッケージのREADME には以下の表明を書いています。

このディレクトリは一時的なものです。将来、各マイクロサービスが独立した API を持つようになった時点で削除されます。 ここに追加したコードは技術的負債です。移管の見通しが立ったら速やかに対応してください。

4. 境界を薄く保つための工夫

ドメインロジックをマイクロサービスに切り出しやすい状態を保つため、私たちは以下の工夫をしています。

公開するのは interface・DTO・ファクトリだけ

microservices/ パッケージが Facade API に公開しているのは、Service インターフェースと、リクエスト / レスポンスの素朴な struct のみです。ドメインロジックやリポジトリへのアクセス方法も、すべて Facade API パッケージからはアクセスできない internal/ の下に閉じています。

呼び出し元の Facade API からは、ドメインの内部実装が完全に隠蔽されています。これは「将来 HTTP 越しの呼び出しに差し替える」ときに、Facade API 側の変更を最小化するための布石でもあります。

依存ルールを go-arch-lint で機械的に守る

パッケージを分けても、ふと油断した隙に microservices/ パッケージから Facade API のパッケージを参照してしまう可能性があります。これを防ぐための便利なツールとして go-arch-lint があります。

go-arch-lint は、プロジェクトの「あるべき依存関係」を YAML ファイルで宣言しておき、実際のコードの import がそのルールに従っているかを静的に検査してくれる Go 用のアーキテクチャリンターです。

私たちは go-arch-lint を使い、microservices/ 配下から Facade API 本体のパッケージを参照できないよう、ルールを設定しました。

microservices:
  anyVendorDeps: true        # 外部ライブラリは OK
  mayDependOn:               # プロジェクト内はここに書いたものだけ
    - microservices
    - ...

これは CI でも検証されます。将来このディレクトリだけを別リポジトリへ切り取れるという性質を、コードベース全体で機械的に維持しています。

5. 得られた効果

microservices/ パッケージを軸としたリファクタをしたところ、以下のメリットを享受できるようになりました。

コードベースの見通しの改善

特に GraphQL リゾルバのコード量が削減され、レビューしやすくなりました。 生成AIによるコード生成の質も安定するようになりました。

将来のアーキテクチャを見据えた議論の活発化

「これは Facade API のロジックか、それともマイクロサービスの責務か」など、未来のアーキテクチャ像を見据えた議論が自然と発生するようになりました。 また、本物のマイクロサービスが建つ前に、コンテキストやドメインの境界を自分たちで自由に定めて試せます。実際のコードで境界の妥当性を検証できるため、将来のサービス分割に向けた予行演習になっています。

アーキテクチャ負債の可視化

我々が頭を悩ませている負債は、ドメインを扱うマイクロサービスがまだ存在しないため、本来そちらが持つべきドメインロジックを Facade API が肩代わりしているものです。そのようなドメインは全て microservices/ パッケージに集められるので、Facade API の抱える構造的負債を明確に可視化することができます。

将来のマイクロサービス分割を機械的に行える期待

依存ルールが守られていれば、極端に言えば microservices/ 配下を Go モジュールとして外部に切り出し、インターフェイスを HTTP に置き換えるだけでマイクロサービスとして独立させられます。移管時の作業は、そこまで見据えて単純化されています。

6. おわりに:「消すために作る」という態度

私たちが作成したmicroservices/ パッケージは、将来 Facade API のコードベースから消える前提のパッケージです。消えることを名前で表明し、機械的なルールで剥がしやすさを担保します。

移行過渡期のアーキテクチャは「美しいアーキテクチャ」ではないかもしれません。ですが、その現実を受け入れて、ありのまま構造化するという選択肢はあっていいはずです。消すために作るパッケージは、消えるその日まで、私たちの目指す将来のアーキテクチャ像を見せてくれます。