Ruby Freaks Lounge

第25回Rackとは何か(3)ミドルウェアのすすめ

前回前々回の記事では、Rackの生まれた背景、Rackとは何か、実際にRackアプリケーションを作る際に使えるものをご紹介しましたが、もう一つまだ説明していない重要な要素がRackにはあります。今回は、そのミドルウェアという仕組みについてご紹介します。

ミドルウェアとは

ミドルウェアとは何かを一言で言うと、⁠別なアプリケーションをラップして、リクエストやレスポンスを加工したり、処理を切り換えたりするRackアプリケーション」です。

この仕組みがあることで一体何ができるのでしょうか。Webアプリケーションを作っていると、リクエストやレスポンスをアプリケーションに行く前やアプリケーションの処理の後に加工したくなることはよくあります。例えば、条件に応じてURLの書き換えをしたり、エンコーディングの変換をしたり、Cookieの処理をしたり…といったことが日常茶飯事です。こういう処理を、サーバとアプリケーションの中間で行なうのがこのミドルウェアなのです。

ミドルウェアの作りかた

まずは、ミドルウェアを作ってみてその挙動を見てみましょう。とは言え、特に難しいことはありません。ミドルウェアが満たしていなければいけない要件は以下の二点です。

  • Rackアプリケーションの仕様を満たしていること
  • newの第一引数に他のRackアプリケーションを取ること

例えば、次のようなコードを書いて、neco_filter.rbという名前で保存してください。

neco_filter.rb
# coding: utf-8

class NecoFilter
  def initialize(app)
    @app = app
  end

  def call(env)
    res = @app.call(env)
    res[2].each do |body|
      body.gsub!(/!|?|。|、/) { "にゃ#{$&}" }
    end
    res
  end
end

そして、config.ruを次のように変更します。

# config: utf-8

require 'simple_app'
require 'neco_filter'

use NecoFilter
run SimpleApp.new

早速rackupでアプリケーションを起動してみましょう。出力が書き換えられているのがわかりますね。前回のSimpleApp同様、NecoFilterもとても簡単なものですが、ミドルウェアを理解する上で重要なポイントがいくつかあります。

まず、元のアプリケーションに一切変更を加えていないということです。出力や入力を書き換える汎用的な処理をこうしてミドルウェアとして実装しておけば、アプリケーションとは独立して付けたり外したりすることができます。また、サーバ側にも依存していないため、例えばApacheeからNginxに切り換えることになったときでも特にアプリケーションに変更を加える必要はなく、同じように使えます。

そして先ほど、ミドルウェアの要件として「他のRackアプリケーション内部に持つRackアプリケーションであること」と言及しました。ミドルウェアそのものもまたRackアプリケーションですので、ミドルウェアを複数重ねることもできます。上記のサンプルではミドルウェアは一つしか使っていませんが、サーバのモジュールやフレームワークのプラグインと同じように、組み合わせて使うことができます。

最後にこれは重要なことですが、Rackベースのフレームワークで実装したアプリケーションに対しても使えるということです。⁠Rackの仕様に則ってWebアプリケーションフレームワークを実装する」ということはすなわち「一個のRackアプリケーションとして動作する⁠⁠、つまりRamazeやSinatraを使って開発されたアプリケーションはサーバやから見れば一つのRackアプリケーションになっているわけです。そのため、先程のNecoFilterミドルウェアも既存のSinatraアプリケーションに適用することができます。こうして汎用的な機能をミドルウェアとして切り出しておくことで、フレームワークをまたいで使うことができるのです。

もちろんパフォーマンスが何より重要な状況であれば、Webサーバのモジュールとして実装した方が当然高速でリソースも節約できます。あるいは、フレームワーク内部のプラグイン機構やフックを利用した方が、自由度はあがるでしょう。ですが、そういう制約がないのであれば、ミドルウェアとして実装されていた方が「サーバとフレームワークを自由に組み合わせることができる」というメリットをより享受できるのです。

余談ですが、ミドルウェアはある決まった形式だというだけのただのRackアプリケーションですので、config.ruで次のように書いても同じ意味になります。

# config: utf-8

require 'simple_app'
require 'neco_filter'

run NecoFilter.new(SimpleApp.new)

useはRack::Builderで用意されたDSLの記法で、後続のミドルウェアやアプリケーションを引数に取ってnewしてHandlerの引数にする、ということをおこなっています。ミドルウェアの要件として「newの第一引数がRackアプリケーションであること」があるのはこの記法を利用するためです。もしuseを使わないのであれば、Rack::URLMapのように、独自のイニシャライザを定義してアプリケーションとして実装してもいいかもしれません。

またconfig.ruやRack::Builderを使わない場合、たとえばCGI等で直接アプリケーションをハンドラに渡す場合は、

use MiddlewareA
use MiddlewareB
run App.new

となっていたものが、以下のような形になります。

Rack::Handler::CGI.run MiddlewareA.new( MiddlewareB.new( App.new ) )

ミドルウェアとRack::URLMap

例えば、以下のようなconfig.ruがあったときに、/aへのリクエストの時は、MiddlewareA → MiddlewareB → Rack::URLMap → MiddlewareC → AppA の順にリクエストが処理され(レスポンスはもちろん逆順です⁠⁠、/bへのリクエストのときは、 MiddlewareA → MiddlewareB → Rack::URLMap → MiddlewareD → AppB の順に処理されます。

# ここでuseしたミドルウェアはURLMapより前に処理を行なう
use MiddlewareA
use MiddlewareB

map '/a' do
  # ここでuseしたミドルウェアは
  # そのパスへのリクエストの時だけ処理を行なう
  use MiddlewareC
  run AppA.new
end

map '/b' do
  use MiddlewareD
  run AppB.new
end

ちなみに、これをRack::Builderを使わないで書くと、以下のようになります。

MiddlewareA.new(
  MiddlewareB.new(
    Rack::URLMap.new(
      '/a' => MiddlewareC.new( AppA.new ),
      '/b' => MiddlewareD.new( AppB.new ),
    )
  )
)

だんだん煩わしくなってきましたね。それでもこの例では、URLMapのマッピングも二つしかありませんし、ミドルウェアもアプリケーションもオプションを指定したりしていませんし、ミドルウェアの連鎖もそれほど多くはありません。当然もっと複雑になることも十分ありますので、そのくらいになると「rackupファイル(Rack::Builder)で記述したほうがスマート」の意味が伝わると思います。

便利なミドルウェア達

Rackをインストールすると、いくつか標準添付のミドルウェアが付いてきます。

例えば、POSTメソッドでDELETEやPUTの代用をしているようなクライアントからのリクエストをそのメソッドでのリクエストであるかのように書き換えてアプリケーションに渡してくれるRack::MethodOverride特定のパスへのリクエストをアプリケーションに渡さずに静的ファイルの内容を返すRack::Staticレスポンスをgzip圧縮するRack::Deflaterあたりは、Webサーバ側で実現することが多いですがよく使う機能です。

開発時には、リクエスト/レスポンスがRackの仕様に沿っているかをチェックしてくれるRack::Lintアプリケーションが例外を出した際に発生箇所や例外の内容やリクエストの中身などを整形してくれるRack::ShowExceptionsサーバ起動中にファイルが変更されたときに自動的に再読み込みしてくれるRack::Reloaderなどにお世話になりそうです。

他には、簡単な認証機構を提供してくれるRack::Auth系、簡単なセッション機構を提供してくれるRack::Session系のミドルウェアも便利です。通常、認証機構やセッションの管理はフレームワーク/アプリケーションの中で実装することになると思いますが、複雑な制御が必要ない場合は簡単に導入できるので重宝します。Basic認証やOpenID認証、CookieセッションやMemchacheセッション等、一通り揃っています。

このようにRack同梱のミドルウェアだけでも結構充実していますし、ミドルウェアの実装の参考にもなりますので、一度目を通しておくことをお勧めします。

ミドルウェアのすすめ

ここまでで、Rackの主な要素については一通りご紹介できたと思います。さて、Rackについてざっと理解できたところで、Rackを何に使えるでしょうか。

これからアプリケーションサーバやフレームワークの開発しようと思っている方には、今あるミドルウェアやRack対応のサーバ/フレームワークの資産を利用することができ、開発の手間を削減する意味でもRubyのWebエンジニアへのアピールの意味でも大きなメリットがあります。本当にシビアなパフォーマンスを要求されるのでバリバリチューニングしたいとか、なにがなんでも一から全部作りたいんだ、というのでなければ、今後はRackの仕様に沿って開発した方が良いでしょう。

そこまでしない方でも、Rackの仕組みはWeb開発における基礎になる部分ですので勉強になりますし、Rackベースのフレームワークの中身を理解する足掛かりになります。また、PassengerのようなRack対応・モジュールタイプのアプリケーションサーバが入っているレンタルサーバが一般的になれば、CGIやPHPの代わりにRackアプリケーションを作るのがWeb開発の第一歩になる日もくるかもしれません。

もちろんそれも素晴らしいことですが、筆者はこの、ミドルウェアを作ることをお勧めします。

前々回、Rackのメリットであり目的であるところは、インターフェースの統一を図ることでWebサーバ/フレームワーク間の組み合わせを自由にすることだと言及しました。しかしながら実際はどうでしょうか。本来普遍的なWeb開発上の課題である事柄を各サーバやフレームワークがそれぞれのアプローチで解決していて、⁠こちらのフレームワークではモバイルサイトを作るのに便利なプラグインがあるが、自分が使いたいフレームワークにはない」⁠構成を変更したくなったが、システムの重要な部分をサーバ側の機能を使って実装してしまっている」なんてことがよくありますし、それが理由で結局特定の環境に縛られてしまうことになります。全部が全部それで解決できるわけではないですが、ミドルウェアが充実してくればより柔軟に対応できるようになり、適材適所でサーバやフレームワークを選択しやすくなります。この記事でも述べたようにミドルウェアの仕様は案外シンプルで、それほど難しくありません。Rackいじりに興味がある方は、まずミドルウェアを書いてみるのはいかがでしょうか。

おわりに

ここまで3回にわたってRackとは何かについてご紹介しましたが、いかがでしたでしょうか。Rackの意義や面白さが少しでも伝わっていれば幸いです。Rackいじりはシンプルな割に奥が深く楽しいので、是非チャレンジしてみてください。

おすすめ記事

記事・ニュース一覧