自己紹介

榎本悠介氏(以下、榎本):「絡み合うSaaSプロダクトのマイクロサービスアーキテクチャ」というタイトルで、絡み合う複数のSaaSプロダクトに対して、業務フローを滑らかにするために、仮にマイクロサービスならどう向き合うかという話をします。

今回話す内容は、完璧な設計やアプローチではなく、現実にあったケーススタディとして生温かく見てください。すごくボリューミーでポンポン行くので、集中して聞いてもらえるとありがたいです。よろしくお願いします。

私は榎本と言います。みんなからはmosaと呼ばれています。Twitterは@mosa_siruです。今はLayerXの取締役 SaaS事業部長をやっていて、プロダクトやエンジニアチームをリードしています。新規事業を作りまくるマンとして、前線で仕様を考えまくってコードを書きまくっています。ボンバーマンがうまいです。

「バクラク請求書」「バクラク申請」について

マイクロサービスの前段として、まずはプロダクトのことを知ってもらい、構成図、ケーススタディ、最後にまとめという流れで進めていきます。

今、「バクラク請求書」「バクラク申請」「バクラク電子帳簿保存」という3つのプロダクトを出しています。どれも単体で使える便利なSaaSですが、今回はバクラク請求書とバクラク申請に注目して話していきます。

簡単にどんなプロダクトかを説明します。バクラク申請は、みなさんも経費精算をやったことがあると思いますが、社内で申請して、稟議・承認(をする)といった一連のプロセスを楽にします。

バクラク請求書は、受け取った請求書を元に、会計処理をしたり振り込んだりといった処理を楽にするプロダクトです。

いろいろなペインがありますが、ここで紹介するのは省略します。

(スライドを指して)Afterバクラク請求書とありますが、大事なのは一連の請求書の流れのプロセスです。

これはバクラク申請でも楽になるし、バクラク請求書でも楽になるし、組み合わせるとさらに楽になるということを言いたいです。発生しがちな請求書の回収漏れを回避するほか、承認が止まっていたり申請の入力が面倒くさかったりする問題を、入力補助したりSlackで承認したりして楽にする機能があります。

さらに、データはバクラク請求書に同期されて、請求書データが作られます。それによって経理は、支払ってよい請求書かどうかがわかります。あとは、請求書の受け取りを楽にしたり、一連の業務フローが作れたりする、うれしいプロダクトです。

今回、ここだけは押さえてほしいというポイントがあります。請求書だけが「払ってください」と来ても、経理は会社のすべてを把握しているわけではないので、払っていいものかわからない。現場で判断してほしい。現場の上長が承認しているか遡る必要があるなど、承認プロセスと経理の請求書処理業務が多くの場合、分断されています。

承認プロセスは専用ソフトで、請求処理業務は印刷して紙で渡されるところが辛い状況にしているので、その2つをシームレスにつなげるプロダクトです。支払いが申請されると、バクラク請求書側に申請の内容とともに請求書データが作られるのが、メチャクチャ楽なポイントです。単体でも便利ですが、組み合わせるとさらに便利です。

システムの構成図

ここからはシステムの構成図の話です。構成図は、プロダクトごとにマイクロサービスで作っています。それぞれフロントエンドと専用のAPIを持っています。専用のデータベースを持っていて、完全に閉じた状態になっています。

他の人のことを調べたければ、プライベートなAPIが立っているので、そこから取りに行きます。例えば、ユーザー情報や認証基盤は別で立っているので、プライベートAPIを通してユーザー情報を取りにいく感じです。

方針は、プロダクトごとのマイクロサービスです。DBはマイクロサービスをまたいで共有しない。ReadもWriteも1人の責任としています。大事なのは、プロダクト間での循環参照を絶対に避けること。デプロイ順が意味不明にならないことをポリシーとして挙げています。

そもそもなぜマイクロサービスにしたのか。それぞれが単体で成立するSaaSプロダクトなので、ドメイン境界として適切であり、使う人も違う点からそう考えました。実際に開発のチームも分かれていたし、それぞれのチームをスケールさせる覚悟も、今後どんどんプロダクトを出す可能性もあったので、プロダクトごとに境界を作りました。

そのほか、複数のプロダクトを展開してつなげているSaaSの先輩たちが、モノリスからマイクロサービスにするのに苦労している事例をたくさん見てきたので、最初からプロダクトごとに作ってみようと思い始めました。

データ同期で循環参照が起きてしまう

ケーススタディです。循環参照を避ける話から始めます。(スライドを指して)データ同期と言いましたが、これが厄介です。

例えば要件(1)。バクラク申請で何らかの申請を作成します。「請求書が来たので払ってください」と申請を上げると、申請の金額や期限などの情報を元に、請求書レコードや請求データをバクラク請求書側で作ります。

例えばどんな構成が考えられるか。申請すると、申請情報を作成します。それを元に請求書データがAPIを叩くなりして、バクラク請求書側で作るといった、Writeという流れが考えられます。

次の要件。そうやって作られた請求書をバクラク請求書側で見る場合は、元になったバクラク申請の申請情報が見えます(Read)。経理が「この請求書はこういう申請フローを通った」「上長がこういう承認をした」と確認するために、元になった申請情報を見たいという要件です。

これをどうやるかというと、請求書を見たい場合は、DBから請求書情報を取ってきて、かつ、元になった申請情報をAPIで取ってくる方法が考えられます。

2つ合わせれば、見事プロダクトごとに循環します。バクラク申請がバクラク請求書を把握でき、逆もしかり。「あぁ、終わった」となるので、これをどう避けるかという話に移ります。

循環を避けるための3つの方法

循環を避ける方法には、Gateway APIを使う、SPA(Single Page Application)が直接参照する、データ同期の3つがあります。それぞれ見ていきます。

まずは、Gateway APIないしBFF(Backends For Frontends)を立てるパターンです。1つのGateway APIが申請あるいは請求書にWriteしていく。逆もしかりで、元の請求書の申請をReadします。こうすれば、申請と請求書間での直接の依存関係はなくなるので、循環参照もなくなるという考え方です。

Gateway APIは自然なのでこれはシンプルな考え方ですが、今は各種事情でGateway APIが存在しません。「そもそもあるべきじゃん」と言われると「ぐぬぬ」と思いますが、今から作るのは大変なのでどうしようという状態です。また、結局Gateway APIがあっても依存関係が発生します。

前のスライドで、バクラク申請にWriteの処理をして、さらにバクラク請求書の処理もすると話しましたが、それをやるとトランザクション境界がまたがっているので、片方が失敗した時にバクラク申請にしかデータが残らないことになります。トランザクションの管理をGateway APIがやるのか、そこまでロジックが持つのかということになります。

そのため、そのロールバック処理は裏側のサービスに任せたほうがいいのではないか。Gateway APIから右に行って下に行くような依存関係が生まれる。依存関係がある時点で、逆に、バクラク請求書から入ってくるパターンで、上に行くと循環参照になる。Writeの考えでいくと、Gateway APIでも依存は発生し得ると思っています。

次にSPA、フロントが直接APIを叩いてしまう。バクラク申請を作るし、申請書を作る。申請を見ることも、請求書を見ることも直接やってしまえというパターンです。

これはBFFを導入する前によく見るパターンです。比較的シンプルですが、これもバックエンドの間で依存が発生し得ます。

さらに、やや複雑な話になりますが、権限まわりがカオスになりがちです。バクラク申請側の人が元の申請を見たい時に、バクラク申請のユーザーでないから権限が足りず見えない事態が起こり得る。サービスとしてよくわからない挙動になり得るというデメリットもあります。

なぜ依存が発生するのかという話に戻ります。先ほどのGateway APIと同じです。Writeをフロントから2つに投げる。申請を作って、それを元にした請求レコードを作るのはトランザクションとしてはわけがわからないので、片方が失敗した時にフロントエンドがどうするのか。結局バックエンド経由になるので、やはり依存が発生すると思われます。つまり、上から下ないし下から上に更新の起点が発生した時点で循環参照になるという気がします。

最後は、必要なデータを同期してしまうという方法です。申請してそれを元に請求書レコードを作りますが、ついでに元になった申請、スライドの下の人がReadしたいものもついでに保持してしまえと。元になった申請も請求書DBなのに、申請情報も一部持ってしまう。必要な分を持ってしまえばDBを見るだけなので、当然Readは簡単です。

(スライドを指して)「Writeは依存するけれど、Readは単体で完結する。なぜならDBに必要なデータが丸ごとあるから」というのがデータ同期のパターンです。

メリットとしては、細かいですが先ほどあったような権限管理の問題で、バクラク申請側に権限がなくて見れない事態を避けられる。デメリットは、データ同期を作るのが複雑。データ不整合の危険性もある。(スライドの)上のDBスキーマを変更した時に下はどうするのかなど、いろいろあります。

よく考えたら出てきた2つの辛い要件

ここからまだ連携の要件が続きます。正直やりながらわかってきたことが多いので、どんどん足していきます。(スライドを指して)よく考えたら出てきた辛い要件(3)です。ややわかりづらいですが、請求書側のリスト面です。バクラク請求書の請求書一覧に、元になった申請のステータスでフィルターしたいものがあります。例えば、承認済みになった申請だけを処理したい。却下されたものはどうでもいい、みたいなことです。経理が元の申請のステータスでフィルターしたいみたいな話が経緯として出てきました。

これはけっこうしんどいですが、あらゆる条件の掛け合わせでフィルターできます。請求書レコードの状態と申請レコードの状態の掛け合わせでフィルターされるので、これを最適なページング込みで実現するには、同じDBでWHERE句のようなAND条件を作っていくしかない。

ゲートウェイでフィルターをしていくと、できあがりのページングがおかしくなるので、もう同じところにデータソースを持つしかないという要件になります。

「よく考えたら出てきた辛い要件(4)」は、請求書レコードを出力する時に、ついでに元になった申請も合わせて出力してほしいという要件です。これもCSVか何かでデータ出力したいと言われたらどうするか、CSVとGateway APIでがっちゃんこロジックを持つのか。

フロントエンドでがっちゃんこするのも現実的ではないので、バックエンドに依存してがっちゃんこデータを作る。そのロジックを書くのはしんどいので、依存は発生するし、「なんだ」という気がしてくる。

結局データを持つほうが楽な気分になってきたので、データ不整合が怖いですが、データをまるっと内蔵してしまう、必要なデータだけというパターンに行くことにしました。

(次回に続く)