Rails + Apartment でマルチテナントな Web サービスを作る

はじめに

例えば Qiita::Team のような、企業やチーム向けに提供するマルチテナントな Web サービスの場合、 テナントを分ける方法としてぱっと思いつくのは

  • すべてのテーブルに tenant_id 列を追加し、tenant_Id で常に絞り込む
  • テナントごとにデータベースを分ける

の2つ。

このうち tenant_id 列を追加する方法だと、使うのに苦労する gem がある。 Devise とか Devise とか Devise とか。いやまぁ、default_scope でやれないことはないかもしれないけど。

テナントごとにデータベースを分ける方法なら、上手くデータベースさえ切り替えられれば、 Devise もそのまま使えるはず。

データベースを分ける方法でマルチテナントを実現するための gem に Apartment がある。

これを試してみる。

まずはサンプルプロジェクト作成

rails new multi_tenant_sample --skip-bundle

Devise と Apartment をインストール

ユーザーもテナントごとに登録したい。 Devise と Apartment を組み合わせて実現出来るか挑戦。

Gemfile に

gem "devise"
gem "apartment"

を追加し、下記のコマンドを順に実行。

bundle install --path vendor/bundle
bundle exec rails g devise:install
bundle exec rails g devise User
bundle exec rake db:migrate
bundle exec rails g apartment:install

テナントを作成

テナント情報を保存するモデルを用意する。 今回は簡略化のため、テナント名を保存するだけにしておく。

bundle exec rails g model tenant name:string

テナント情報は、デフォルトのデータベースにまとめて保存したい。 Apartment が Tenant を除外するように、config/initializer/apartment.rb を編集する。 あと、テナントのデータベース名一覧を取得するための設定も記述しておく。

# config/initializers/apartment.rb
require 'apartment/elevators/subdomain'

Apartment.configure do |config|

  # Tenant を除外
  config.excluded_models = %w{Tenant}

  # ... (中略) ...

  # rake db:migrate 時にすべてのテナントのデータベース名を取得するための設定
  config.tenant_names = lambda{ Tenant.pluck :name }
end

# Apartment でデフォルトでは、サブドメインでデータベースを切り替える
Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'

tenant_names を設定していないとマイグレーションを実行できないので、先にやっておくこと。 そして

bundle exec rake db:migrate

を実行。

無事マイグレーションが実行できたら、デフォルトのデータベースにテスト用のテナント情報を登録する。

bundle exec rails c
irb(main):001:0> Tenant.create(name:"foobar")
irb(main):002:0> Tenant.create(name:"hogefuga")

テナントを登録したあと

bundle exec rake apartment:create
bundle exec rake db:migrate

を実行すると、db 下に foobar.sqlite3 と hogefuga.sqlite3 が作成され、それぞれマイグレーションが実行される。

トップページを作成

確認用にトップページを作成する。

bundle exec rails g controller home index

トップページには登録されているユーザーの一欄を表示したいので、コントローラーとビューを下記のように修正する。

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @users = User.all if user_signed_in?
  end
end
<h1>Home#index</h1>
<% if user_signed_in? %>
  <%= link_to "logout", destroy_user_session_path, method: :delete %>

  <ul>
    <% @users.each do |user| %>
      <li><%= user.email %></li>
    <% end %>
  </ul>
<% else %>
  <%= link_to "login", new_user_session_path %>
<% end %>

ルーティングも修正。

# config/routes.rb
MultiTenantSample::Application.routes.draw do
  devise_for :users
  root "home#index"
end

ローカル環境で動作確認

Pow を使って動作確認する。Pow のインストール手順は次の記事の通り。 rbenv 使っていると、ちょっと苦労する。

Pow の準備が終わったら、‾/.pow の下に rails プロジェクトへのシンボリックリンクを作成。

cd ‾/.pow
ln -s ‾/Projects/multi_tenant_sample

『登録したテナントの名前=サブドメイン=テナントが利用するデータベースの名前』になっているので、 ブラウザで http://foobar.multi_tenant_sample.dev/users/sign_in にアクセスしてみる。

f:id:griefworker:20140330172959p:plain

サインアップ。

f:id:griefworker:20140330173034p:plain

登録されているユーザーの一欄が表示される。

次に http://hogefuga.multi_tenant_sample.dev にアクセス。

先ほどサインアップしたユーザーでログインしてみると

f:id:griefworker:20140330173354p:plain

ログインに失敗し、ログイン画面に戻る。ちゃんとデータベースが切り替わっているみたいだ。

こちらでもサインアップ。

f:id:griefworker:20140330173530p:plain

ユーザー一欄には、サインアップしたユーザーだけが上がっている。

まとめ

Apartment を使うことで、テナントごとにデータベースを分け、 サブドメインで切り替えることができた。 Devise に Apartment を組み合わせることで、テナントごとにユーザーを保存するのもうまくいった。

今後、Rails でマルチテナントな Web サービスを作るときは、まず Apartment を使うことにしよう。