RailsエンジンとPackwerkによるコード分割を進行中
Railsでサービスを開発 / 運用をしていると、コードの肥大化に伴うモノリシック化に悩まされることも多いはず。2014年のサービス開始からRailsで進めてきたnoteも今まさにその壁に立ち向かっている最中です。
Railsアプリケーションを分割しようと考えたときに、マイクロサービス化や別言語でのフルリプレイスなどを検討することもあるはずです。
様々な選択肢がある中で、弊社ではPackwerkの導入とRailsエンジン化による分割を進めることにしました。(※ packwerk:Shopifyが作成したgem。依存関係をパッケージによって整理することができる)
Railsエンジンを採用した大きな理由としては以下が挙げられます。
すばやく小さく問題を切り分けることを優先
マイクロサービス化はアーキテクチャから考慮する必要があり時間がかかる
将来的なマイクロサービス化の下準備として進めることができる
Railsのレールに乗って開発が進められる
エンジン化が難しかった場合、手戻りの工数も少ない
開発自体はまだまだ進行中で、実際に取り組んで メリットもデメリットもあるなと実感しました。
そこで今回の記事では、現在の状態でエンジン化を行った理由や課題などにそれぞれ説明していきたいと思います。
※ この記事はnote株式会社のアドベントカレンダー1日目の記事です。明日は「SnowflakeのTerraform化」を公開する予定です。
なぜRailsエンジンで分割するのか?
noteの分割を考えたときに、当初は「投稿機能だけをマイクロサービス化する」など機能単位で切り出すことを検討していました。
しかし、プロダクトを運用しながらマイクロサービス化していくことが難しいのは明白ですし、リプレイスのために既存の機能開発をストップさせる可能性もあります。
また、機能を切り出す際には、依存関係の整理やDBの分割も考えねばなりません。アーキテクチャを1から検討するリソースも必要です。言語を変更するのであれば、学習コストも上がるでしょう。
そこでnote社では、まずはPackwerkによるモジュラモノリス移行を行うとともに、Railsエンジンによる分割をしていくことにしました。Rails内で依存関係を整理し、今以上に複雑なコードにならないように制御することにしたのです。
PackwerkとRailsエンジンの組み合わせであれば、スモールスタートで始められるうえに、既存のプロダクト開発にほとんど影響がありません。
また、エンジンに切り出す際には依存関係の確認や整理を行うため、将来的なマイクロサービス化の下準備にもなります。大きく改修するのではなく、困難を分割して問題を小さく解決していく道を選びました。
手探りでの作業
もともと社内にRailsエンジンの知見が少なかったため、始めは手探りで開発をスタートしました。それこそ「エンジンなら分割できるのでは?」「無理だったとしても戻せばいいしやってみようか!」くらいの気持ちでした。Railsエンジン自体が枯れた技術で参考資料も豊富にあるため、安心感があったためです。
また、リプレイスや分割などの作業にエンジニアの人数をそこまで割けないという実情もあります。(どの企業でもそうだと思いますが)
そういったリソースの意味合いでもスモールスタートで進められるのは、エンジン化に踏み出した大きな理由でもあります。
Packwerkとエンジンの違い
RailsエンジンのコードはPackwerk配下で管理しています。特別なことはなにもせず、違和感なく連携ができています。エンジンとPackwerkを使って分割していくことに、現状では問題を感じていません。そもそも、それぞれで切り口がまったく異なります。
Packwerk
モノリシックなコードの見通しをよくして開発しやすくする
依存関係の整理や管理を行う
Railsエンジン
独立できる機能を切り出す
将来的にマイクロサービス化を検討している機能などを分割する
現状の課題
依存関係を分離できているわけではない。まずは第一段階としてのエンジン化
Railsエンジンでの分割は、ユーザー側に影響がない管理画面やデバッグ機能から始めています。しかし、現状で完璧に分割ができているわけではなく、課題が多く存在しています。
あくまで現状は、分割の第1段階としてのエンジン化のような状態です。
具体的な課題としては、エンジン化したアプリケーションが親Railsに依存している状態で、親のモデルをエンジンが利用しています。
最初は、エンジン化とともにモデルを完全に分離も試みたのですが、やはりDBの完全分離が壁でした。一足飛びでは解決できないことがわかったため、まずは依存ありきでエンジン化をすることにしました。
これだけ聞くと、「コードを分割するだけのエンジン化に意味があるのか?」と思われるかもしれません。しかし、今このタイミングでエンジン化を行うことに意義があると考えています。
これからの依存の増加を抑制することができますし、全体の依存からエンジンの依存だけを考えればよくなります。もちろん「どんな機能でもエンジン化すればいい」という話ではなく、あくまで将来的に分割できる機能を検討して慎重に行うことが重要です。
RSpecの依存
エンジンが親Railsへ依存しているため、RSpecの依存も解消しきれていません。
現状はエンジン配下にテストを置いているだけのような形です。「specの置き場所がエンジンにもある」というようなイメージが近いかもしれません。
マイグレーションファイルの置き場所について
親Railsとエンジンでマイグレーションファイルを分けるかどうかの議論を行ったのですが、分散させない方針にしています。deviceなどのgemの設計を参考に、親にマイグレーションファイルを持たせることにしました。
しかし、あくまで親Railsとエンジンが同じDBを見ているという事情があるための設計です。別々のDBを見ているのであれば、マイグレーションファイルも分けた方がいいかもしれません。
エンジン化して起きた変化
最後にエンジン化によって起きた副作用的な効果を3つほど紹介したいと思います。
将来的に独立したサービスになる可能性がある新機能はエンジン化を検討する
親Railsに依存しない大きな新機能を作るのであれば、エンジン化を前提に開発することを検討するようになりました。
エンジンで作った機能がサービスとして軌道に乗ったときには、別Railsアプリとして切り離して運用することが容易になります。逆に、機能として失敗だった場合にも削除が簡単に行うことができます。
routesの整理
サービス拡大に比例してroutesの見通しが悪くなっていました。「いつかは改修しなければ……」と腰が重かったのですが、エンジン化をキッカケに整理することができました。
# hoge, fugaのようにエンジンを指定するだけでroutesが設定できる
draw(:hoge) if Rails.env.production?
draw(:fuga)
また、エンジンをマウントするかどうかをroutesで設定することができるため、将来的には各サーバーで読み込むかどうかを決めることができるようになりました。
Packwerkで親Railsからエンジンへの依存を禁止する
エンジンから親Railsへの依存はまだある状態ですが、親Railsからエンジンへの依存はPackwerkで禁止しています。Packwerkで設定を行うことで、今後は依存関係の増加を防ぐことができます。
スモールスタートで始めるならエンジンの検討もあり
プロダクトのフェーズやコードの規模にもよりますが、モノリシックなRailsを分割する手段として、スモールスタートで素早く始めるのであればエンジンはおすすめです。
エンジンを利用せずに言語もフレームワークもフルリプレイスする方法ももちろんありますが、リソースがかなりかかる作業です。また、長年稼働している依存関係が多いサービスを改修するのであれば、まずはエンジンで小さく分割していくのがいいでしょう。
マイクロサービス化を進めて敷居の高さに頓挫してしまうこともあると思うので、まずはエンジン化やPackwerkの検討をしてみてもいいのではないかと思います。
▼noteエンジニアアドベントカレンダーはこちら
▼さらにnoteの技術記事が読みたい方はこちら