モジュラモノリスを導入に向けて境界分けをどうするか

志賀誠氏:じゃあ今度は、モジュラモノリスの実現の方法について説明します。(スライドを示して)Railsでモジュラモノリスを導入するにあたって、パッと思いつくもので、このスライドにあるような問題があるかと思います。

1個は、やはりRailsはRubyなので、なんでも書けちゃうということがあると思います。もうやろうと思ったらいくらでも越境できちゃう境界区域とかがあると思います。

もう1個はActive Recordが強力ですよね。「Arel」もあると思いますが、けっこう自由に書けるので、「どこのテーブルに対して何のクエリを投げる」という制限をかけるのを真面目にやろうとするとなかなか大変だというような思いがあって。このあたりをhacomonoでどんな感じで対応したのかを説明します。

弊社も例に漏れずにpackwerkを使っています。packwerkの詳細については先ほどタイミー社さんから説明があったので、私のほうでは触れる程度にします。

弊社の場合だと、packagesみたいなディレクトリを切っていて、その中にパッケージを何個も作っているような状態です。論理的分割をしています。弊社もタイミー社さんと同じで、パッケージごとにコードオーナーを設定しています。これによって、「『GitHub』でほかのチームが修正した」とか、そういったことを検知できるようにして、安心して運用できるようにしようという取り組みをしています。

packagesごとにYAMLでルールを設定する

(スライドを示して)packwerkはYAMLでルールを設定するんですが、ざっくりとここに書いてあることができるようになっています。「エントリーポイントを外部のパッケージに公開するメソッドがこれですよ」みたいなものを定義できるエントリーポイントの定義と、パッケージが依存できるパッケージ。例えば「共通基盤パッケージ」と言えばいいですかね。メール配信だったりの共通の機能は依存したくなると思うので、そういったルールも書けるようになっています。

あとは「パッケージが一時的に依存するnamespace」と書いてあるんですが、既存のコードベースをパッケージ化する時に、一気にやるのは無理だと思うので、一時的な許容ができる仕組みがあります。「RuboCop」を使ったことがある方だと「.rubocop_todo.yml」みたいなものがあるイメージですね。

あとはパッケージのステータスですね。そのチームのパッケージを公開中か開発中かみたいなものも設定できるようになっていて、bundle execコマンドで静的に精査ができるようなものになっています。

パッケージの公開エントリーポイントに対する2つの定義付け

弊社の場合だと、これに加えてもう1個取り組みを入れていて、パッケージの公開エントリーポイントに対しては、protoでどういうリクエストをもらうかと、どういうメソッドが生えているかを定義するようにしています。

protoファイルを各チームでパッケージを用意するチームが作って、それをRubyのコードにコンパイルして生成されたRubyのクラスを用いてやり取りするみたいなイメージです。後でちょっと実例のコードを紹介したいと思います。

protoを使っている背景は2点あります。冒頭の検討で「モジュラモノリスとマイクロサービス、どっちにするか?」みたいな話があったと思うんですが、弊社の場合だと、マイクロサービスはGoで作ろうという意思決定があるんです。

そうなった時に、protoでAPIの部分を定義しておいたら、protoを使ってそのままマイクロサービス化も比較的推進しやすいというのがあって、protoを一枚かませている状況です。

あとはRubyなので、protoの定義に沿った引数を渡してもらうという制約を設けることで、なんでも渡せちゃうという状況を排除している状況ですね。型チェックぐらいの役目としてprotoを入れています。

そのprotoからRubyのコードを生成するところは、gRPCが公式で出している「grpc_tools_ruby_protoc」というものがあります。これをprotoファイルにかませると、そこでRubyのコードが作られるイメージです。

(スライドを示して)ざっくり例が書いてあるんですが、mailer.protoみたいなものがあった時に、protoファイルに対してコンパイラをかませると、2個の成果物ができるんですね。

「mailer_pb」がメッセージみたいな情報を持っていて、RPCがどういうリクエストとレスポンスを扱うかの情報を持ったRubyのクラスみたいな「mailer_service_pb.rb」が生成されます。

実際の例です。プロダクトのコードそのままではないんですが、「こんなイメージで使っていますよ」というコードになります。見えなかったら後でスライドの確認をお願いします。

継承しているクラス、Mailer::MailService::Serviceがprotoコンパイラが生成したほうのクラスで、GetMailが私が実装したクラスですね。GetMailはRPCの名前で揃えている状況です。

外からの使い方としては、1クラス1RPCみたいなかたちでファイルを分割していて、それぞれのクラスがexecuteメソッドを持っている。外からAPIっぽく呼べるようなファイル構成にしています。

肝になるところが、rpc_descというやつが真ん中ぐらいにあると思うんですが、それが親クラスで持っているメソッドで、この中で「そのRPCがどういうリクエスト、レスポンスを扱うか」みたいなものが取れる、便利なメソッドなんですね。

このrpc_descを処理の前と後に挟んで、「GetMailクラスはどういうレスポンスとリクエストを扱うか」みたいなバリュエーションに今は利用しています。

(スライドを示して)これはベタで書いているんですが、共通Concernみたいなやつに切り出して、それぞれのクラスにincludeしているようなかたちです。こういう仕組みを入れています。

gRPCで通信しているわけじゃないんですが、これによって「protoのファイルに沿ったやり取りができていますか?」ぐらいの簡易チェックをするものとして利用しています。コードの例は以上です。

永続化層へのアクセスは簡易的な制限を実施中

次が、永続化層への制限をどうやっているかですね。コードベースで分かれていたとしても、そのパッケージがアクセスするデータベースが有象無象だったらあまり意味がないので、データベースへのアクセスの制限も簡易的なものですが作っています。実現する手段としては、「Arproxy」という、クックパッドさんが作っているgemを利用しています。

これに関してすごく簡単に話すと、ArproxyはActive Recordとデータベースアダプターの間をフックして好きな処理を挟めるみたいなgemで、今回はそれを利用しています。

使い方の例です。(スライドを示して)「ざっくりとこんなことをしていますよ」という図になります。外部からのパッケージのアクセスが来たタイミングで、RailsのThread.currentにそのパッケージが許可するテーブル一覧を突っ込んでいます。

そのパッケージ内部で実際にクエリを呼ぶところです。「QueryObserver」というArproxyのプラグインを実行すると生SQLが流れてくるんですが、それに対して愚直に正規表現を書いています。ちょっと小声になっちゃいますけど。

100パーセントは弾けませんが、今の弊社のRailsの使い方だと、Railsなのであまりアソシエーションとかを貼っていないんですね。比較的シンプルなクエリが流れてくるので、今はそれでだいたい弾けているような状況です。

これはちょっと想像に難くないんですが、本番で動かすとたぶん重いので、今のところデベロップとステージングとQA環境で動かしています。「検知したい」というのは顧客にぜんぜん関係なく、開発者都合なので、「開発段階で気づけたらいいや」という背景でそのようにしています。

Rails generatorの「hacoway」も活用

最後です。すごい細かい取り組みもしています。今僕が口頭でバーッと説明したことをエンジニアの人に「じゃあ、やって」と言うと、たぶんなかなか難しいと思うので、Rails generatorで「hacoway」というものを作っています。「hacoway」は「hacomono way」の略なんですけど。これでモジュール名を渡して実行すると、パッケージとprotoファイルを含めて全部自動生成できるみたいな仕組みを用意しています。

以上です。あっ、忘れていました。最後にちょっとリクルートになってしまうんですが、ここまでの話を聞いて、お客さまの中に「WOW!」を届けるような基盤作りに興味を持っている方がいたら、ぜひお声掛けいただけると幸いです。

また、選考に進まなかったとしても、単純に「ここらへんの技術、どうなっているの?」みたいな、気になっているところとかがありましたら、Twitter(現X)で@maco_tasuにメンションしてもらえたら答えるので、質問などをお待ちしています。

以上となります。ご清聴ありがとうございました。