はっさんブログ

自分の発見を共有します。

機能が増えたRailsアプリケーションをRails Engineで分割する【vol.1】

参考になったらシェアいただけると幸いです!

背景

Railsの開発を続けていると、機能が増えてアプリケーションを分けたいなと思う時があります。

例えば、メインアプリ・管理者画面・API (内部/外部)...etcがあって、それぞれでのみ使用するコードが共通部分に記載されていると管理が大変になります。

Rails Engineを使うことでMVC、扱うgem、assets、initializers等をEngineごとに分離させることができます。

また、各Engineを同一リポジトリ内で管理することができ、ディレクトリ構成もスッキリさせることができます。

Rails Engineとは

Rails アプリケーションを拡張する「部品」です。

詳細はRails Guideが詳しいので、是非一度目を通してください。

railsguides.jp

エンジンとアプリケーションは、細かな違いを除けばほぼ同じものであると考えていただいてよいでしょう。
エンジンとアプリケーションは、同じ構造を共有しています。

とありますね。

Rails アプリ本体は土台、Engineは部品で、その構造が類似しているイメージです。

有名どころではdeviseactive_admin等がRails Engineを用いて開発されています。

deviseは認証機能、active_adminは管理者機能を提供しています。

今回の利用方針

deviseactive_admin がEngineを用いて開発されていることを知り、Engineを用いる際は他アプリへの導入のしやすさが大切かと思っていました。

それもできたことに越したことはないのですが、単純にアプリケーションを切り分けるために使うのも有効な使い方です。

様々なアプリケーションから利用できると便利なRails Engineですが、今回は単純に機能をEngineに分離することを目的とし、他への移植性は考えません。

それらを踏まえて、今回は以下のような構成にしました。

f:id:usgitan:20180203194024j:plain

本来では Rails エンジンによる脱 Microservices 化のススメにあるように、モデルをplugin化し、各Engineからmodelライブラリを利用する形になるかと思います。

しかし、リンク先の 複数アプリケーション同居のつらいところ にあるデプロイやmigration周りに不安があったためモデルは本体側に置きました。

このおかげでデプロイやmigrationは特に変更なくEngine化に踏み切ることができます。

Rails Engineの作成手順

前置きが長くなりました。

それでは、実際にEngineを作成していきます。

# 環境

Ruby 2.1.4
Rails 5.0.2

Rails Engineの作成

bundle exec rails plugin new [Engine名] --mountable (以下お好みで) -d postgresql

以下からはEngine名を main_app としていきます。

環境変数でインスタンスの役割を判断し、Bundlerで読み込むgroupを追加する

# config/application.rb
groups = Rails.groups

groups << :main_app if ENV['SERVER_TYPE'] == 'main_app'

Bundler.require(*groups)

これで環境変数 SERVER_TYPE == 'main_app' が設定されてる時のみ、Gemfileにある:main_app groupをBundlerに読み込ませることができます。

つまり、main_appの役割をするインスタンスでは SERVER_TYPE == 'main_app'を、例えばAPIだったら SERVER_TYPE == 'api'を設定してあげることで読み込むgemを分割することができます。

本体のGemfileにEngine化したgemを追加する

# Gemfile
group :main_app do
  gem 'main_app', path: 'main_app'
end

path はEngineのディレクトリを指定します。

例えば apps/main_app のようなディレクトリ構成にしたい場合は、そのように指定してあげれば良いです。

本体の方でbundle installを通すために、作成したEngine内のgemspecで"TODO"となっている部分を適切な内容に変更します。

# main_app/main_app.gemspec

$:.push File.expand_path("../lib", __FILE__)

# Maintain your gem's version:
require "main_app/version"

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
  s.name        = "main_app"
  s.version     = MainApp::VERSION
  s.authors     = ["hassan"]
  s.email       = ["hassanhogehoge@gmail.com"]
  s.homepage    = "https://github.com/(リポジトリ名)"
  s.summary     = "main_app"
  s.description = "main_app"
  s.license     = "MIT"

  s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]

  s.add_dependency "rails", "~> 5.0.2"

  s.add_development_dependency "pg"
end

bundle install が正常に通ることを確認します。

ルーティングにEngineへのパスを追加する

# config/routes.rb

if defined?(MainApp::Engine)
  mount MainApp::Engine => '/', at: :main_app
end
# main_app/config/routes.rb

MainApp::Engine.routes.draw do
  root to: 'home#index'
end

それでは実際に環境変数を設定して、rails routesを実行し、Engineへのパスが表示されているかを確認してください。

# .envrc
export SERVER_TYPE="main_app"
$ rails routes

Routes for MainApp::Engine:
     root GET    /                  main_app/home#index

表示されていればOKです。

次回

続きはvol.2に移ります。

www.hassansan.me

余談ですが、Ruby製のHanamiというフレームワークは最初からアプリケーションごとにディレクトリが切られているようです。

Rubyのマイクロサービスフレームワーク Hanami!! Railsとの違いを説明しながらCRUD機能を作成する - Qiita