mod_mruby の可能性とディレクティブ設定/スクリプトの例


これは [mod_mruby ngx_mruby Advent Calendar 2014](http://qiita.com/advent-calendar/2014/mod-ngx-mruby) の１０日目(12/10)の投稿です。

今回は mod_mruby および Apache を、WEBアプリケーションのコンテンツ配信サーバとして利用する際に、便利そうなスクリプトを幾つか書いてみたので紹介します。

尚、前提となる環境は、アドベントカレンダーの３日目に書きましたので、そちらを参照ください。
→ [mod_mruby を Amazon EC2、Apache2.4 へ導入する](http://qiita.com/hkusu/items/ca8622c83b0bfc2cac72)
→ Apache のバージョンは `2.4.10`、mod_mruby のバージョンは `1.9.7` です。

# 私の考える mod_mruby の利点

Apache の各フェーズにフックできるので、例えば PHP、Perl等での WEBアプリケーションをそのまま生かしながら、機能を拡張できる点かなと。図にしてみました。（実際にはリバースプロキシとしても使えます。）

![スクリーンショット 2014-12-09 16.42.18.png](https://qiita-image-store.s3.amazonaws.com/0/35733/9e4ad147-0056-8f4a-c227-d41eb6377d5e.png "スクリーンショット 2014-12-09 16.42.18.png")

よくある処理を共通モジュール化しておけば、各WEBアプリケーション開発のコード量を減らせますし、またWEBサーバが Apache であれば（ngx_mruby であれば Nginx）、異なる言語のプロジェクトでも横断して利用することが出来ます。

これらは元々は Apache モジュールとして C言語 で書けばいいのですが、大変なので^^; mod_mruby として Ruby のシンタックスで簡単に書ける ＆ [mgem](https://github.com/mruby/mgem-list) で拡張できるというのは強みかと。

# Apacheのディレクティブ設定にあたり気づいたTips

- 基本的に mod_mruby の設定ディテクティブ（例：`mrubyHandlerMiddle`）は `.htaccess` には書けないので、もし書くなら各種 `.conf` に書きます。
	- `.htaccess` が煩雑になるので、もっともな仕様だと思います。
	- が、Apache の標準ディレクティブの中には `.htaccess` に書けるものがあるので、フックするフェーズによっては、それと組み合わせることも可能かと。

		```apacheconf:.htaccessの例
		AddHandler mruby-script .rb
		（.conf で事前に mrubyHandlerEnable On としておく必要がある）
		```

		Apache のハンドラの説明についてはこちら
		→ http://httpd.apache.org/docs/current/ja/handler.html
		
		フィルタについは未検証ですが、たぶん同じようなことが出来るんじゃないかな。
		→ http://httpd.apache.org/docs/current/ja/filter.html

- URLが○○だったら適用する、というパターンを実現するには、次の２通りの方法があると思います(後者は未検証ですが、たぶん大丈夫)。
	- Apache の `Location` `Directory` `Files` 等でリクエストを絞った上で、mod_mruby スクリプトを適用する。

		```apacheconf:例
		<Location /mruby>
		    mrubyHandlerMiddle "/path/to/unified_hello.rb"
		</Location>
		```

	- mod_mruby のスクリプト内で判定する。
		- URL が望むものでなければ `Apache::return(Apache::OK)` で終了する。
		- ちなみに `Apache::return(Apache::OK)` `Apache::return(Apache::DECLINED)` は、クライアントにレスポンスを返すのではなく、Apache の次フェーズに処理を移す、という挙動のはずです。
- フックするフェーズ（例：`mrubyAuthChecker***` `mrubyFixups***` `mrubyLogTransaction***` `mrubyOutputFilter` 等）によってアクセスできる mod_mruby のクラスやメソッドが異なるので（Apache の処理フェーズ上の仕様）、もし動かなければ違うフェーズを試すのが良いかと。
- 同じフェーズに複数の mod_mruby スクリプトは適用できないので、やるとしたら `***First` `***Middle` `***Last` でバラけさすか、もしくは スクリプトの設計(Rubyの設計)で、１スクリプトに複数機能を持たせるか、になると思います。
	- ファイルの分割とか出来るのかしら？

その他、Apache の概要や設定ディレクティブについては、公式ページを見ると良いかと思います。
→ http://httpd.apache.org/docs/current/ja/

# スクリプト例

前提として下記の例は、URLでは絞らず、全てのHTTPリクエストに適応する例で書いています。絞る場合は、上記に書いた Tips を参考に絞っていただければ。

フックする Apache の処理フェーズは、特に理由がなければ `mrubyFixupsLast` としています。
理由としては「PHP」「Perl」「HTML/JS/CSSの静的コンテンツ」そのほかの「コンテンツハンドラ」を起動する直前のフェーズであり、他の Apache モジュールの影響を受けにくいというのと、単に私が好きだからです。

> ちなみに私は、過去に Apache モジュールの開発経験があったりします^^;

最後のコンテンツハンドラの例を除いて、上記の「私の考える mod_mruby の利点」に書いたとおり、他のコンテンツハンドラ(PHP等)をそのまま生かして機能を拡張する例です。

## ① クライアントの接続元IPによってアクセス制限する

WEBアプリケーションの開発中に接続元の IP アドレスで制限をしたくなることは多いと思いますが（古くはガラケーサイトをキャリアのゲートウェイIPアドレスで制限など）、これを mod_mruby で設定する例です。
もしくは、ネットワーク的に クローズドな API サーバの場合など。

```apacheconf:***.conf
mrubyFixupsLast /usr/local/apache2/mruby/ip_check.rb 
```

```ruby:/usr/local/apache2/mruby/ip_check.rb
e = Apache::Env.new
c = Apache::Connection.new

white_list = [
  '111.222.333.444',
  '222.333.444.555',
  '333.444.555.666'
]

if e['ENV']=='development'
  if white_list.include? c.remote_ip
    Apache::return(Apache::OK)
  else
    Apache::return(Apache::HTTP_UNAUTHORIZED)
  end
end
```

ホワイトリストに含まれない IP アドレスからの HTTP リクエストは、Apache の認証エラーにしています。

## ② クライアントのユーザエージェントによりコンテンツを切り替える

よくありそうな、クライアントデバイスの種類による提供コンテンツの切り替えです。動的に生成するコンテンツの場合はコンテンツハンドラ側(PHP等)で切り替えれば良いのですが、下記の例では mod_mruby により静的コンテンツに対応させています。

```apacheconf:***.conf
mrubyFixupsLast /usr/local/apache2/mruby/agent_content.rb
```

```ruby:/usr/local/apache2/mruby/agent_content.rb
r = Apache::Request.new

if /iPhone/ =~ r.headers_in['User-Agent']
  /^(.+)\.html$/ =~ r.filename
  r.filename = $1 + '_iphone.html'
end

Apache::return(Apache::OK)
```

iPhone で `hoge.html` にアクセスしたら `hoge_iphone.html` を適用させる、という内容です。

## ③ HTTPレスポンスヘッダを付与する

コンテンツの配信時に、ブラウザのキャッシュを有効利用するために、HTTPレスポンスヘッダを付与する例です。

> ブラウザキャッシュとHTTPレスポンスヘッダの関係はこちらを参照ください。
> → [静的リソース(HTML,JS,CSS,画像)のブラウザキャッシュを制御](http://qiita.com/hkusu/items/468cc0ee0d767e7cc790)

クライアントのブラウザキャッシュを有効利用することで、WEBサーバに対するアクセス負荷を軽減できます。

```apacheconf:***.conf
mrubyFixupsLast /usr/local/apache2/mruby/add_res_header.rb
```

```ruby:/usr/local/apache2/mruby/add_res_header.rb
r = Apache::Request.new

if /\.html$/ =~ r.filename
  r.headers_out['Cache-Control'] = 'max-age=0'
end

if /\.(css|js)$/ =~ r.filename
  r.headers_out['Cache-Control'] = 'max-age=600'
end

if /\.(gif|jpe?g|png)$/ =~ r.filename
  r.headers_out['Cache-Control'] = 'max-age=86400'
end

Apache::return(Apache::OK)
```

配信するコンテンツの拡張子によって、キャッシュさせる時間を切り替えています。

## ④ コンテンツハンドラでの出力を更に置換する

上記の①〜③では `Fixups` フェーズ（コンテンツハンドラの直前）にフックしていましたが、コンテンツ生成後のフェーズにフックする例です。
「PHP」「Perl」「HTML/JS/CSSの静的コンテンツ」等の出力内容を `OutputFilter` フェーズで操作します。

```apacheconf:***.conf
SetOutputFilter mruby
mrubyOutputFilter /usr/local/apache2/mruby/convert_content.rb
```

```ruby:/usr/local/apache2/mruby/convert_content.rb
f = Apache::Filter.new

content = f.flatten
f.cleanup

f.insert_tail content.gsub /index/, 'hoge'
f.insert_eos
```

上記は単純に `index` という文字列を `hoge` という文字列で置換する例です。

このフェーズでの置換は、私は可能性を感じています。WEBアプリケーション開発で何かお決まりの処理を、Apache のモジュールとして搭載できるからです。
（コンテンツのサイズが大きい場合のメモリ利用量は注意かも？）

## ⑤ おまけ：コンテンツハンドラとして mod_mruby を適用する

簡易なものを除いて、WEBアプリケーションのコンテンツ生成で mod_mruby を利用することはツール等を除きそんなに多くはないかと思いますが、設定例です。

基本的には `.conf` で `mrubyHandler***` で mod_mruby のスクリプトを指定すればいいのですが、mod_mruby ではハンドラ名 `mruby-script` が公開されているので、次のように mod_mruby をハンドラとして起動することが出来ます。

事前に `.conf` で mod_mruby をハンドラとして起動できるようにしておき、

```:***.conf
mrubyHandlerEnable On
```

`.htaccess`(別に`.conf`に書いてもいいんだけど)で、リクエストコンテンツの拡張子が `.rb` ならハンドラとして mod_mruby を適用します。

```:.htaccess(もしくは***.conf)
AddHandler mruby-script .rb
```

こうすることで、`mrubyHandler***` で mod_mruby スクリプトを指定するのに比べて **任意の(ファイル名の) mod_mruby スクリプト** を動的に(`.conf`の設定なしに)適用することが出来ます。

> この場合、スクリプトの cache ができない気もしますが。

また応用として、mod_rewrite からハンドラ起動も出来ます。

```apacheconf:.htaccess(もしくは***.conf)
RewriteEngine on
RewriteRule \.rb$ - [L,H=mruby-script]
```

この例では単純に `.rb` で判定していますが、mod_rewrite なので、更にいくらでも柔軟に条件を指定できるかと。

# おわりに

mod_mruby での開発については、昨日の [@matsumotory](https://twitter.com/matsumotory) さんの記事（[mod_mrubyインストール後入門](http://qiita.com/matsumotory/items/7121d6cce728125e5ce2)）が分かりやすかったので、そちらも参照いただくと良いかと思います。

それ以外には [mod_mruby の wiki](https://github.com/matsumoto-r/mod_mruby/wiki) を見たり、[mod_mruby のサンプルソース](https://github.com/matsumoto-r/mod_mruby/tree/master/example) を読むと、作り方が分かるかと。

明日(12/11)は takeswim さんによる「nginxにngx_mrubyを導入する」です。よろしくお願いします :smile:

> １６日目はまた私のターンなので、そこでは Amazon S3 へのリバースプロキシについて書く予定です。