法改正をマイクロサービスで立ち向かう(後編)
はじめに
ボリュームが多くなってしまったので前編・後編の2部構成になり今回は後編となります。
前回の記事は こちら
Engineers Advent Calendarなのでエンジニア向けの記事になります。
お題
前回の記事で法改正に対応するにあたりドメインをシンプルにする為に
同一の関心事を時系列で分断させ別の関心事として扱う
という、ドメイン分割するアプローチについて説明させて頂きました。
稼働中のサービスに対してドメイン分割を行う際にどう取り組んで実現していったのか、具体的に検討したことや課題も含めて書きたいと思います。
マイクロサービスのドメイン分割する際の事例として参考になれば嬉しいと考えています。
実現したいイメージは以下になります。
前提としてSPAとして構築しているものとなります。
-
As-Is
-
To-Be
まず考えたこと
今回分割する関心事は元々一つです。具体的にはレセプト業務の関心事のドメインを扱うAPI(以下レセプトAPI)があり、そのAPIを水平分割するイメージです。
レセプト業務のフロントエンドアプリ以外からも複数の業務(フロントエンド&バックエンド)が呼び出されます。
時系列で処理するAPIを呼び分ける必要があるのですが、まずどうやって呼び分けるか?ということを考えました。
呼び出し元のアプリ側でも同じ時系列のキーは持っており、呼び出し元で判断して呼び出し先のAPIを変更することは可能です。
ただ以下のシステム特性があり、方針の検討が必要だと考えました。
- 分割するAPIに対して呼び出し元が多岐にわたる
SPAで構築されており、バックエンドはPHP Laravel、フロントエンドはvue, nuxt(javascript)と言語・フレームワークも複数存在し依存しているアプリの種類も多い [1] - 分割されるAPIが今後も増え続ける
3年毎にドメイン分割していきます - プロダクトとして機能追加は活発である
法改正でもAPI分割と同時に新規APIの追加があります
また以下も懸念としてありました。
- 改修範囲が広く、リソース的に修正できるか?
法改正の改修中に他チームに依頼して対応可能だろうか? - 分割後のAPIが最初は存在しないが、いつから改修を始めるか?
分割後のAPIの完成をまってから、呼び出し元の改修〜検証まで行うのは難しいのでは? - 言語・フレームワークも複数あり対応漏れや品質のばらつきが発生しないか?
まず影響調査が必要ですが、対応漏れが一番怖いです。
同じAPI呼び出しでフロントエンドとバックエンドとで挙動が変わったら嫌すぎます。。 - レセプトAPIの数が多く検証どうやってやろうか?
レセプトAPIは75ほどあり、分割した後でもロジック的には変わらないAPIもあり、正しく呼び分けられているか?を一つづつ確認するのが大変です。
法改正のメイン改修でリソースが割かれて、一つづつ検証する余裕はなさそうです。 - 法改正で追加されるAPIやリリース以降で追加されるAPIで対応漏れを発生させてしまうのでは?
追加されるAPIに対して確実に振り分けできるようにしたいです。 - 次の法改正でも同じ対応コストがかかる?
また3年後の法改正でバタバタしたくない。今回の対応によっていい感じにして今後のメンテナンスコストを下げたいです。
前編で法改正への対応の難しさについて触れましたが、時間的制約があるので法改正のメイン開発に着手する前までになんとか見通しを立てたいと思いました。
誰がAPIを呼び分けるか?
APIの振り分けの責務はどこに持たせるべきか?という点について考えたいと思います。
- いつから法改正が施行されるか?
- それを知っているのは誰でその責務を持つのか?
上記を考えた際に
- レセプトAPIが自身の扱う制度とその適用期間は把握している
- 逆にレセプトAPI以外の業務では関心の範囲外
- 分割後の新・旧のそれぞれのレセプトAPIはお互いの扱う制度の適用期間は知らなくていい
と考えました。
特に2については強く認識しているものです。
- レセプト業務の関心事が別領域に分散してしまう
- 呼び出し元側で振り分けるビジネスルールを組み込みたくない
それぞれ上述した懸念につながるもので、絶対避けないといけないものです。
そこで それを知っているのは誰でその責務を持つのか?
については レセプトAPIに近いレセプトAPI以外の何か
と考え
振り分けルール自体を新しい一つの関心事
として切り出すことにしました。
こうすることで、レセプト業務に関連している他の業務にレセプト業務の関心事を分散させず一箇所に閉じ込めれると判断しました。
検討した振り分け手法
振分けルールを一箇所にとめて実現させる方法として以下の3つが候補としてありました。
- 振り分けの為の時系列のキー情報をAPIのエンドポイントURLのパスパラメータとして定義し振り分けを行う
https://example.com/api/rezept/${振分けキー}/calculate
みたいな感じです。ルールベースにすることで、呼び出し元側では裏で動いているアプリケーションは意識しなくて済みます。
パスパラメータであればAmazon API GatewayやNginxのレイヤーでも振分けを行えるメリットがありそうです。 - 振り分けルールをモジュールとして実装し、各アプリに展開させる
各アプリで共通で利用できるsubmoduleが既にあり、そこのモジュール内にロジックを閉じ込めてしまう方法です。
サービスプロバイダー経由でAPIアクセスさせたり、HTTPクライアントからのAPI通信をhookさせたりできます。
直接APIの呼び出し先を変更するのでパフォーマンスや障害発生ポイントが少ないというメリットがありそうです。 - Proxyアプリケーションを作成し、そこで振り分けを行わせる
実装のコストがかかりますが柔軟性があります。
ただ、APIの数が75 [2] あり、スケジュール的な制約の影響を受けやすいデメリットが考えられました。
上記3つの候補の実現性と優位性を確認する為に、まず現状把握を行いました。
- API仕様書を精査
- 呼び出し元のリスト化
現状把握を行った結果、以下のことがわかりました。
- 単純振分けでは実現できない
大きく分類すると5パターンがあり、それぞれ対応を行う必要がありました。 - 呼び出し元となる影響をうけるアプリがフロント・バックエンド両方にそれなりにある
ある程度想定はしていましたが、やはり数が多く、多岐に渡っていました。
上記の結果から、以下の方針としました。
Proxyアプリケーションを実装する
個別振分けのパターンをそれぞれ実装し、個別振分けのパターン以外については共通の振分けルールとして実装する
以下のようなイメージとなります。
新しく追加したProxyアプリケーションのアプリ名はレセプトAPIへのルーティングのみを行うので rezept-router
と命名しました。 [3]
こちらの構成にすることで、呼び出し元は今まで通り言語やフレームワークに関係なくAPIを呼び出しすることができます。
また呼び出し元の対応も必要最小限で、今後追加されるAPIに対しても対応漏れが発生しづらくなります。
次の法改正でも rezept-router
に振分け先アプリを追加するだけ済みます。
rezept-routerの実装
rezept-routerで実現する機能としては「リクエストパラメータに含まれる時系列のキー(提供月)を判断してリクエストを振り分ける」です。
この提供月の指定のパターンと、レスポンスの扱いによって振分け処理が異なってきます。
APIは以下の5つのパターンに分類されると整理しました。
- 提供月が一つしか存在せず、単純な振り分け
- 提供年月が複数にまたがり、更新APIを実行しステータスコードのみ返却する
- 提供年月が複数にまたがり、レスポンスを統合する必要がある
- 提供年月が存在せず全バージョンのAPIを呼び出す必要がある
- 単純に提供年月が存在しない
困ったのが5のパターンです。どうやって振り分けしたら良いのか?判別できません。
ただユースケースの想定としては提供月に関連付けされていて、パラメータとして定義していなかっただけでした。
対応としてはAPIのI/F仕様上のみ提供月を追加し、呼び出し元で指定するパラメータに提供月を付与する様に対応を行いました。
対応の結果、5のパターンが1のパターンとして統合され4つのパターンのみ実装すれば良くなりました。
今後のAPI追加する際のルールとして必ずパラメータに提供年月を含めることとしてチーム内に共有しました。
1のパターンであればrezept-router自身に手を入れなくても対応可能となります。
また、呼び出し元のリスト化した結果、既に使用されていないAPIが存在したがあったので屠り作業 [4] を行いました。
それぞれ対応によって、パターンの整理・統廃合され、75あるAPIのルーティングの定義は最終的に18種類の実装で済みました。
rezept-routerへの切り替え
振分けの関心事がうまく切り出しができ疎結合に保つことができた為、当初懸念していた
- 分割後のアプリがない状態で並行開発ができるか?
- 振分けの動作検証を行えるか?
- 調査漏れ等で対応漏れが発生しないか?
という点がそれぞれ
- 他のアプリへの影響も少なく、並行して開発が進められるようになった
ビッグバンリリースにならなくて済み、法改正対応前に事前リリースできた。 - ルーティングの処理はLaravel APIとして実装でき単体テスト可能になった
APIのモック処理は php-vcr を使い実際のレスポンスをキャプチャしたものを利用して検証を行いました。 - 切り替え時の動作検証が楽になった
.envを切り替えるだけなので検証観点の粒度がアプリ単位で済むようになりました。
上記で解消されました。
他の検討した手法に比べてテスタブルになりました。事前に十分品質を確認でき、切り戻しも簡単になりました。
リリースは段階的に行うことにし、更にリスク低減の為に以下の対応を追加で行いました。
- 法制度開始前に新アプリを呼び出されてもエラーになるようにマスターデータの削除、バリデーションの追加
- 逆に法改正開始後に間違ってルーティングされてもエラーになるように、旧アプリのマスターデータの有効期限を変更した
こうすることで、最悪振分け処理に誤りがってもエラーとして検知でき、データ不整合を防げます。
関心事を分離してしまったので、関心事以外のリクエストが来た場合にエラーにする必要があります。
一番怖いのは振分けが間違っていてそのまま誤請求になることなので、しっかりと対応を行いました。
後編まとめ
前回の記事に書きましたが、ドメイン分割の方針は決まっていたものの、いざ蓋を開けてみると色々課題がありました。
関心事を分割する上で検討や配慮が足りなかった点があったと思います。
具体的には上記の振分けパターンの種類が多かった件で、1つのドメインで複数の関心事にまたがってしまっていた点です。
フロントエンドのユーザビリティ向上の為、複数提供月を跨いだり、提供月関係なく全てのデータを取得するユースケースがありました。
それ自体は問題ないのですが、APIの仕様として1回のリクエストで処理する仕様になっており、ドメイン分割にあたり関心事が追加され複数関心事を扱う結果になってしまいました。
フロントエンド側でAPIの呼び出し回数が増えてしまっても提供月単位で処理を行うAPI設計が望ましかったのでは?と思いました。
あと、本記事のスペースの問題で詳細には書きませんが、別ドメインのデータベースと共有していた箇所があった為、データベースの分離も必要でした。
インフラリソースの効率化の為、初期リリース時の判断として共有することとしました。
こちらもそれ自体は問題ないのですが、レセプトAPI内でデータベースの分離予定のテーブルを直接参照している処理がありました。そのテーブルはレセプトの責務外となるので、該当の処理を本来あるべきドメインのAPIとして切り出し直しました。
法改正プロジェクト開始直後はドメイン分割の影響範囲がわからず、不安がありました。
ただAPI呼び出しのトレーサビリティを高める対応 [5] を入れたり、今回まとめたような責務の再定義をして切り分けができ結果的に良かったと感じています。
ここまで整理してできていれば、次回の法改正では今回よりも安心して望めると思います。
ただ安心感を持続させるにはドメインの責務を綺麗に保つというのが重要になります。
今後の改修では責務をより意識して設計を行っていきたいと思います。
全体振り返り
前編・後編とで法改正をマイクロサービスで立ち向かうというテーマで書きましたが、如何でしたでしょうか?
複雑なものを関心事という単位に区切って責務を閉じ込めるということをどのような視点や考え方で行っていっているかが伝われば嬉しく思います。
関心事をどの視点でどう捉えるかによってサービスの切り口が変わってくるという所がマイクロサービスを設計する上で難しいと感じています。
今回のケースではその時点では最適な切り口だと思っていたものが、外部環境の変化(今回は法改正)によって関心事が増えてドメイン分割をする判断をしました。
実際に切り出しする際も将来像を予想しながら行いました。業務のドメイン知識がないと予測も難しいです。
まだ自分自身も手探りで完全に正解とは言い切れないのですが、ただ今回は一定の成果は出せたのではと考えています。
サービスの責務が明確になり適切に切り分けられ「各チームがそれぞれの関心事に集中して取り組む」ことができたのでは?と思っています。
法改正の対応では五月雨で上がってくる情報をキャッチアップして各チームがそれぞれ影響範囲を考えてアプリの改修を並行で行う必要があります。
関心事の分離が出来ておらず責務が曖昧なままだと、安心して並行開発を行うことができません。
システムが複雑かつ時間的制約があるなかで、集中して取り組む環境がなかったら法改正に立ち向かえなかったと思っています。
最後に
明日はいよいよ最後です。
SREの@tjinjinです。
今年7本目(!)となる記事を投稿してくれます。お楽しみに!
Discussion