Heroku でマルチテナントな Rails アプリを実現する

はじめに

Web アプリでマルチテナントをやる場合、 すべてのテーブルに tenant_id を追加してデータを分けるのではなく、 テナントごとにデータベースを分けたい。

データベースを分けておけば、バグで他のテナントのデータまで変更してしまう可能性を減らせる。

データベースを分けたマルチテナントをサポートする、 『Apartment』という gem を使った記事を以前書いた。

Heroku 上での運営を考えているので、 Heroku 上でもテナントごとにデータベースを分けたマルチテナントが可能か試してみた。

なお、Heroku Postgres を使うから、厳密にはテナントごとにスキーマを分けることになる。

Heroku にデプロイ

Rails + Apartment でマルチテナントな Web サービスを作る - present で作成した Rails プロジェクトを使うことが前提。 git で Heroku に push する。

$ git remote add heroku git@heroku.com:tnakamura-sandbox.git
$ git push heroku master

テナント作成

テナントを登録するテーブルが必要なので、まずマイグレーションを実行する。

$ heroku run bundle exec rake db:migrate

Heroku 上で rails console を実行し、テスト用のテナントを登録する。

$ heroku run bundle exec rails console
irb(main)001:0> Tenant.create(name:"foobar")
irb(main)002:0> Tenant.create(name:"hogefuga")

テナントのスキーマを作成

テナントを登録したので、各テナントのスキーマを Heroku Postgres 上に作成。 その後、各スキーママイグレーションを実行する。

$ heroku run bundle exec rake apartment:create
$ heroku run bundle exec rake db:migrate

今回は rake タスクで実行したけど、スキーマの作成と削除は Tenant のコールバックにしたほうが良さそうだ。

サブドメインを登録

Qiita::Team みたいに、テナントごとに異なるサブドメインを使いたい。

テナントが増えるたびにサブドメインを登録するのは手間なので、 例えば次のような、ワイルドカードサブドメインを登録する。

cname *.yourdomain.jp tnakamura-sandbox.herokuapp.com.

自分はバリュードメインを使っているので、バリュードメインで設定した。 末尾のピリオドは必須。これを忘れたせいで、1日潰してしまった。要注意。

Heroku にドメインを追加

バリュードメインで登録した、ワイルドカードサブドメインを Heroku に追加する。

$ heroku domains:add *.yourdomain.jp

これで、例えば foobar.yourdomain.jp と hogefuga.yourdomain.jp で、 Heroku 上の Rails アプリにアクセスできる。

Heroku 上でテナントごとにスキーマが違っているか確認

Web ブラウザで、まず foobar のサブドメインにアクセス(実際のドメイン名は都合により隠している)。

f:id:griefworker:20140725215141p:plain

サインアップしてみる。

f:id:griefworker:20140725215209p:plain

ログイン後のトップページにユーザーの一覧が表示された。

次に hogefuga のサブドメインにアクセス。

f:id:griefworker:20140725215238p:plain

サブドメインが違うので、こちらではまだログインしていない状態になっている。

こちらでもサインアップしてみる。

f:id:griefworker:20140725215320p:plain

すると、ユーザー一覧にはサインアップしたユーザーだけが表示された。

f:id:griefworker:20140725215353p:plain

ちゃんとテナントごとにスキーマが分かれているようだ。

まとめ

ワイルドカードサブドメインを使っているので、 テナントごとに異なるサブドメインを使える。

また、Apartment を使っているので、サブドメインごとにスキーマを分けることができた。 結果、テナントごとにスキーマを分けることに成功。

ワイルドカードサブドメインと Apartment を使うことで、 Heroku でもマルチテナントを実現できた。

死にたくないのでエチケットカッターを買った

Wikipedia によると、鼻毛を抜くと死ぬ可能性があるらしい。

鼻毛は脳に近い場所にあるため、鼻毛を抜くと毛根が黴菌により炎症し化膿、脳に影響が及んで死亡する場合がある

死因が「鼻毛を抜いたから」というのは勘弁。エチケットカッターを買うことにした。

買ったのは、ネットで評判が高かったパナソニック製のエチケットカッター。

届いたので早速使ってみたけど、予想外にくすぐったかった。 自分が敏感なだけかも。 鼻の中央側はハサミで切りにくくて神経使っていたけど、エチケットカッターだと簡単。

あと、眉を整えるのにも使えるのも嬉しい。 鼻に使ったやつを眉にも、ってのは抵抗あるけど、水洗いすれば気にならない。 最低でも月に1回掃除した方がいいらしい。

2160 円と値段は高くないし、買ってよかった。オススメ。

Rails のベストプラクティスに従っているかチェックするために rails_best_practices を導入

Rails の使い方が間違っていないかどうかを、人が目視でチェックするのは、 無駄ではないけど時間がもったいない。

もしツールでチェックできるとしたら、ツールでやった方がいい。 設計とか、他にもっとレビューするべきところはあるのだから。

そこで rails_best_practices という gem の出番。

この gem は、Rails Best Practices というサイトで公開されているベストプラクティスに、 Rails アプリが従っているかをチェックしてくれる。

gem install rails_best_practices

でインストールしたら、Rails プロジェクトのルートディレクトリで

rails_best_practices -f html .

を実行。すると rails_best_practices_output.html が出力される。 こんな感じのやつ。

f:id:griefworker:20140716205020p:plain

あとは警告を1つずつ潰していけばいい。

Vim で ^M を一括削除

vim

Rails でジェネレーターを実行したら、ファイルの末尾に余分な改行コード ^M が追加されてしまったので、Vim でまとめて削除する方法をメモしとく。

コマンドモードに切り替えて

:%s/^M//g

を実行すればいい。 なお、^MCtrl-V Ctrl-M を押して入力する。

牛亭

肉が食べたい気分だったので、 六本松駅から歩いて15分、大濠公園すぐ側にある『牛亭』に行ってみた。 この店は人気らしく、ランチは予約しないとすぐ満席になってしまうみたい。 そうとも知らず予約なしで行ったら、運よくカウンター席が開いていた。 危なかったな。

f:id:griefworker:20140705114645j:plain

A ランチ(ポーク炭火焼+ハンバーグ)と手造りハンバーグステーキがよく注文されているみたいで、 どちらにするか迷ったけど、今回はハンバーグを注文。

f:id:griefworker:20140705115727j:plain

ハンバーグは鉄板いっぱいに広がっていて、予想以上のボリューム。 ソースはデミグラスソース。やっぱりハンバーグはデミグラスでしょ。 ソースをよく絡めて食べると旨い。

f:id:griefworker:20140705114947j:plain

サラダはコールスローみたいだった。

f:id:griefworker:20140705115743j:plain

ライスとパンを選べたので、ライスを選択。 肉にはライスが一番合う。

あと、アフターのコーヒーがついて、1200 円(税抜)。 増税の影響か、値段が上がっていて少々お高い印象を受けた。 以前は 1050 円(税込)だったらしいので、200 円以上の値上がり。 1080 円だと良かったのに。 まぁ、ハンバーグはボリュームあったし、コーヒーも付いていたから、 そう考えると 1200 円(税抜)でも納得だな。

関連ランキング:ステーキ | 六本松駅唐人町駅大濠公園駅

Grape で API を複数ファイルに分けて定義する

Grape では 1 ファイルに API をずらっと書けるけど、 規模が大きくなってくると見通しが悪くなるので、複数ファイルに分割したくなる。

Grape では API が他の API をマウントできるので、 その機能を使ってリソースごとにファイルを分割できる。

以下、サンプル。ヘルパーも別ファイルに切り出してみた。

posts.rb
# coding: utf-8
module API
  class Posts < Grape::API
    resource :posts do
      desc "投稿をすべて取得"
      get do
        # posts はヘルパーメソッド
        posts
      end

      desc "投稿を1件取得"
      get ":id" do
        posts.find(params[:id])
      end
    end
  end
end
helpers.rb
# coding: utf-8
module API
  module APIHelpers
    def posts
      Post.order(:id)
    end
  end
end
api.rb
# coding: utf-8
require_relative "helpers"
require_relative "posts"

module API
  class API < Grape::API
    format :json
    default_format :json
    prefix "api"
    version "v1", using: :path

    helpers APIHelpers
    mount Posts
  end
end

gem をオフラインでインストールする方法

あらかじめ Web に繋がったマシンで .gem ファイルをダウンロードしておけば

gem install --local <gem ファイル名>

でインストールできる。 ただし、依存する gem がインストールされていないと失敗するから、それらをすべて手動でインストールする必要があるため苦行。

Rails アプリで使う gem をまとめてオフラインインストールしたい場合、あらかじめ

bundle package

でインストールしている gem を vendor/cache に出力。 そして、まだ bundle install していないプロジェクトに vendor/cache をコピーして

bundle install --path vendor/bundle --local

を実行できれば、まとめてインストールできる。 依存している gem をいちいち手動で入れる必要がないので、こちらは楽。