[Rails] Rails の動作環境と Location について

最近、プログラミング系のトピックについて全然書いていなかったので、今日は久しぶりに Ruby On Rails について書きます。

今回は複数 Rails 環境を VirtualHost ではなく、Location の違いによって実装する方法についてです。

背景

多くの Ruby On Rails アプリケーションを解説する文章では、virtual host 機能を利用して、/ の Location が Ruby On Rails として使われていることを前提に説明がなされています。
しかしながら、DNS の設定の権限を持っていない場合などでは、virtual host の利用は現実的な解ではありません。他に、http://localhost:3000/ などと、ポート番号指定で行うという方法もありますが、ポート番号指定はあまり好きではない方もいらっしゃるでしょう。そういう人たちのために、1つのサーバで Location を変える事で複数の Rails アプリケーションを提供する方法について説明します。

実現したいこと

http://localhost:3000/depot/ で depot アプリケーションに、 http://localhost:3000/typo/typo アプリケーションにアクセスできるようにしたい。

実現方法

Apache + FastCGI の場合

この場合は、httpd.conf に設定が必要です。

/etc/httpd/conf/httpd.conf に次の設定を行います。


  Options ExecCGI FollowSymLinks
  RewriteEngine On
  RewriteRule ^([^.]+)$ $1.html [QSA]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]


Alias /depot/ "/home/cuzic/depot/public/"
Alias /depot "/home/cuzic/depot/public/"

apache2 を再起動

これだけで、正しく動作します。
このとき、

url_for(:admin => "admin",:action => "index")

は、次のようになります。

/depot/admin/index/

これは、apache の場合は Rails が url_for の出力を正しく処理するような機構があるからです。それは、「relative_url_root」です。これについては、後ほど lighttpd の説明を行うときに合わせて詳しく説明します。

あと、/depot/admin/index ではなく、./admin/index 等のように相対パスで表示させたい場合は、Relative Path プラグイン を利用します。こちらについても、lighttpd と共通の内容なのでそちらで説明します。

lighttpd の場合

lighttpd の場合は、次のようにします。

> cd depot # in your environment
> cp /usr/lib/ruby/gems/1.8/gems/rails-1.1.2/configs/lighttpd.conf config/
> emacs -nw config/lighttpd.conf

このコピー先のディレクトリについては、Ruby On Rails で検証用の場合のディレクトリです。恒久的な利用の場合は、/etc/lighttpd/lighttpd.conf 等の方が良いでしょう。

lighttpd の最後に次のような記述を追加

$HTTP["url"] =~ "^/depot/" {
  server.document-root = "/home/cuzic/depot/public/"
  alias.url = ( "/depot/" => "/home/cuzic/depot/public/" )
  server.error-handler-404 = "/depot/dispatch.fcgi"
fastcgi.server = ( ".fcgi" =>
  ( "localhost" =>
  ( "socket" => "/tmp/depot.socket1",
     "bin-environment" => ("RAILS_ENV" => "development"),
     "bin-path" =>  "/home/cuzic/depot/public/dispatch.fcgi",
     "strip-request-uri" => "/depot"
)))
}

このとき、server.modules に mod_alias を追加することを忘れないように注意してください。

そして、次のコマンドを実行します。

script/server lighttpd

server.modules に mod_alias の追加を忘れていると、alias.url の行でエラーメッセージが出力されます。あと、/usr/sbin/ 等 lighttpd が存在するディレクトリに PATH が通っていない場合にもエラーメッセージが表示されます。そういう場合は、PATH を通してあげるようにしましょう。

このとおりにすると、lighttpd でも正しく実行されますが、url_for の出力に問題があります。

url_for(:admin => "admin",:action => "index")

は、次のようになります。

/admin/index

これは、/depot/admin/index となるべきです。


簡単にこの問題を解決するためには、2つの方法があります。拙作の Relative Path プラグインを使う方法と、先ほど説明した relative_url_root を使う方法です。

Relative Path プラグインを使う場合は、次の手順に従います。

> script/plugin install http://opensvn.csie.org/relative_path/trunk/
> mv vendor/plugins/trunk/ vendor/plugins/relative_path/
> emacs -nw app/controllers/application.rb
class ApplicationController < ActionController::Base
  include RelativePath
end

以上で、

url_for(:admin => "admin",:action => "index")

は、次のようになります。(現在のディレクトリによりますが)

./admin/index

Relative Path プラグインを使うことで、簡単に1つの lighttpd で複数の Location で 複数の Rails アプリケーションに対応するように Rails アプリケーションを開発できます。

もう1つの方法が、relative_url_root を使う方法です。

config/environment.rb の最後に次の行を追加してください。

ActionController::AbstractRequest.relative_url_root = "/depot"

こうすることで、

url_for(:admin => "admin",:action => "index")

は、次のようになります。

/depot/admin/index

けれど、この方法は Rails アプリケーション側のコンフィグファイルである environment.rb に /depot という文字列が入っているのが気になります。これは、http サーバ側に行うような種類の設定で、Rails アプリケーション側にあるべき設定ではありません。apache では設定不要なのに lighttpd のために設定しているというのも気になる点です。

調査した結果、これを解決するためには、次のように relative_url_root メソッドを再定義する方法を見つけました。

もともとの relative_url_root は次のとおりです。

module ActionController
  class AbstractRequest
    # This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
    # It can be automatically extracted for Apache setups. If the server is not
    # Apache, this method returns an empty string.
    def relative_url_root
      @@relative_url_root ||= case
        when @env["RAILS_RELATIVE_URL_ROOT"]
          @env["RAILS_RELATIVE_URL_ROOT"]
        when server_software == 'apache'
          @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
        else
          ''
      end
    end
  end
end

上記の

        when server_software == 'apache'
          @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')

の箇所が、WEB サーバが apache の場合の relative_url_root のデフォルト値を設定している箇所です。この行と同じことを lighttpd でも行わせるような記述をすれば、url_for を実行することで、適切な URL が生成されます。

このことが分かれば、 environment.rb の最後に次のように書くことで再定義しましょう。

 class ActionController::AbstractRequest
   def relative_url_root
     @@relative_url_root ||=
       case
       when @env["RAILS_RELATIVE_URL_ROOT"]
         @env["RAILS_RELATIVE_URL_ROOT"]
       when ENV["RAILS_RELATIVE_URL_ROOT"]
         ENV["RAILS_RELATIVE_URL_ROOT"]
       when server_software == 'apache'
         File.dirname(@env["SCRIPT_NAME"].to_s)
       when server_software.include?("lighttpd")
         File.dirname(@env["SCRIPT_NAME"].to_s)
       else
         ''
       end
   end
 end

これで、lighttpd の場合でも server_software == 'apache' と、同じ動作をするようになります。なんとなく、File.dirname を使うような形でリファクタリングも行ってみました。動作条件が多少変化しています。

なお、コメントに書かれていますように、このような記述をしなくても、本当は RAILS_RELATIVE_URL_ROOT という環境変数に対して、設定してあげれば、正しく動作はずです。そうするには、次のように lighttpd.conf を設定します。

$HTTP["url"] =~ "^/depot/" {
  server.document-root = "/home/cuzic/depot/public/"
  alias.url = ( "/depot/" => "/home/cuzic/depot/public/" )
  server.error-handler-404 = "/depot/dispatch.fcgi"
fastcgi.server = ( ".fcgi" =>
  ( "localhost" =>
  ( "socket" => "/tmp/depot.socket1",
     "bin-environment" => ("RAILS_ENV" => "development",
       "RAILS_RELATIVE_URL_ROOT" => "/depot"),
     "bin-path" =>  "/home/cuzic/depot/public/dispatch.fcgi",
     "strip-request-uri" => "/depot"
)))
}

しかしながら、これは私の環境では動作しませんでした。理由は不思議なのですが、ActionController::AbstractRequest#env["RAILS_RELATIVE_URL_ROOT"] が正しく設定されていないことが原因のようです。ENV["RAILS_RELATIVE_URL_ROOT"] は設定されているので、不思議です。AbstractRequest#env と ENV の違いによるもののようですが、これ以上の調査は私には出来ませんでした。

なお、この点を考慮して、先ほどの relative_url_root はメソッド定義されています。RAILS_RELATIVE_URL_ROOT の環境変数が定義されていれば、そちらを優先する仕様になっています。

と書きつつ、1つの lightttpd で 複数の Rails アプリケーションを提供するためには、これだけ説明を書かなければいけないという時点で、イケてないですね。

結論としては、1つの lighttpd で複数の Rails アプリケーションを提供したい場合は、Relative Path プラグインを使う方法が一番、お気楽な方法でしょう。(手前味噌ですが)

参考文献
http://d.hatena.ne.jp/moro/20060126/1138286609
http://rubyonrails.org/api/classes/ActionController/AbstractRequest.html