thanks to @bamchoh https://github.com/bamchoh/mod_mruby_logo
ngx_mrubyのロゴも募集中です!(このロゴを緑ベースでngx_mrubyとするだけでも良いかも...)
おはようございます。@matsumotory でございます。Qiitaを使って本格的に記事を書くのは初めてですので、どうぞ色々よろしくお願いします。
この記事は、mod_mruby ngx_mruby advent calendar 2014の9日目の記事になります。8日めは @mookjp さんの「mod_mrubyとDockerを使ってプレビュー環境を作成するプロキシサーバを作った」でした。
なんとなんとmod_mruby ngx_mruby advent calendarはこれまで素晴らしい記事が途絶える事無く連続して続いており、作者としても非常に感無量でございます。
1日目にはmod_mruby・ngx_mrubyの作者としてどういう記事を書くべきか考えて、内部アーキテクチャのお話やそれに関する論文公開をしました。
で、これからどういう事をadvent calendarの記事にするか悩んでいたのですが、そういえばインストール後にmod_mrubyとngx_mrubyをどのように触っていくか、といったようなインストール後の最初の一歩の記事をこれまであまり書いてなかったような気がします。
なので、今回はmod_mrubyインストール後入門ということで、インストール後にどうmod_mrubyでApacheの振る舞いを実装していくかにおけるポイントを説明したいと思います。これを読むと、mod_mrubyを試しやすくなるのではないか、と思っております。
インストールの仕方については、advent calendar 3日目において @hkusu さんが素晴らしいmod_mrubyインストール手順を書いてくれたので、それに続こうと思います。
mod_mrubyとは
mod_mrubyを触ってみようと思った時点で、幾つかやりたいことが決まっている人も多いですが、mod_mrubyはまだまだ運用やプロダクション導入による実績はこれからというところで、色々な問題が見つかっていくだろうと予想しています。もちろんそれらは随時改善していく予定です。
ですが、ちょっと触ってみようかな、とか、検証してみようかなと思ってmod_mrubyをとりあえずインストールした時に、インストールしてから何をやってみようか考える場合もあると思います。ここでずばりmod_mrubyはなにかというのを一行で表すと、
mod_mrubyとはApacheモジュールをRubyで効率良く実装するためのApacheモジュール
といえます。ここでの効率良さとは、Rubyの生産性だけでなく、mod_mrubyの使いやすさや性能面等も含まれます。その点の詳細に関しては、1日目の記事を御覧ください。
つまり、mod_mrubyをインストールした後は、ApacheモジュールをRubyで書いてみよう、というように考えると色々としっくりくるわけです。
そこで、今回はmod_mrubyとApacheモジュールの関係を軸に、ApacheモジュールをどのようにRubyで実装していくかの考え方を説明していきたいと思います。
ディレクティブ入門
ApacheモジュールをC言語で実装する際には、Apache内部で起動時から設定ロード時、さらにはリクエスト処理からレスポンスを返す処理において、関数ポインタを渡しておくことで、各種フックでCで実装された関数が呼び出し実行されます。
これが基本的なApacheモジュールの仕組みになります。その関数内で、様々なApache APIを呼んだり、構造体の内部を読んだり上書きしたりすることで処理を実現します。
mod_mrubyはこの各種フックをmod_mrubyというモジュールでwrapして、各種フックにRubyコード、あるいは、Rubyスクリプトファイルを渡せるようにしました。そして、本来C言語の関数内で呼ぶApache APIや構造体の操作を、Rubyのクラスとしてwrapし、メソッドで同等の処理を行えるようにしています。
つまり、mod_mrubyのディレクティブでは、ApacheモジュールのC言語開発における関数ポインタフックのwrapperなわけです。
フックポイントとディレクティブ名
その考え方でいくと、mod_mrubyの基本的なディレクティブの書き方として、
<Location /mruby>
# 上から順に呼び出され処理が進んでいくイメージ
mrubyAccessCheckerMiddle /path/to/check.rb
mrubyFixupsMiddle /path/to/rewrite.rb
mrubyFixupsLast /path/to/rewrite_last.rb
mrubyHandlerMiddle /path/to/content.rb
</Location>
というように書くと、まずmrubyAccessCheckerMiddle は、Apache内部のaccess_checkerというフックポイントにおける中間Middleで/path/to/checkr.rbというRubyスクリプトを実行します。その内部ではApache APIや構造体を操作するメソッドが実行され、Apacheが理解できるCの関数が実行されるという流れになります。
つまり、check.rbというコードは、access_checkerで呼ばれるApacheモジュールのC言語関数と同等である、と考えることができますね。
また、access_checkerという同じフックポイントでも、ディレクティブ名の最後にFirst・Middle・Lastと書くことで、同一のフックポイント内で順番を決めることもできます。それを実際に書くと、上記のようにmrubyFixupsMiddleやmrubyFixupsLastと書くことができ、Lastと書かれているディレクティブの方が同じフックポイントでも後で実行されます。
そして、mrubyAccessCheckerMiddleやmrubyFixupsLastあたりの処理が問題なく通ると、レスポンスを生成するためのフックポイントであるmrubyHandlerMiddleが実行され、ここに実装されているような処理を元にレスポンスが生成され、クライアントに送信されます。
また、mrubyHandlerMiddleが無ければ/mruby 以下にあるファイル群が通常通りコンテンツとして返されることになりますし、もし別のファイルを返すような処理がmrubyFixupsLast等に書かれていたりすると、それに従ったファイルをレスポンスとして返します。例えば以下のようなコードです。
r = Apache::Request.new
if 条件
r.filename = "/path/to/other.html"
end
こういったフックポイントとmod_mruby上でのディレクティブの命名は基本的に同一の命名規則で名前を決めています。それらを以下のように表にまとめました。Apache内でのフックの順序も大体は表の上から順に実行されていくと考えてもらって良いです。(厳密には色々ありますが)
基本ディレクティブ
| Directive名 | C言語上のフックポイント名 | フック内での順番 | 引数1 | 引数2(optinal) |
|---|---|---|---|---|
| mrubyPostConfigFirst | ap_hook_post_config | APR_HOOK_FIRST | file path | "cache" |
| mrubyPostConfigMiddle | ap_hook_post_config | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyPostConfigLast | ap_hook_post_config | APR_HOOK_LAST | file path | "cache" |
| mrubyChildInitFirst | ap_hook_child_init | APR_HOOK_FIRST | file path | "cache" |
| mrubyChildInitMiddle | ap_hook_child_init | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyChildInitLast | ap_hook_child_init | APR_HOOK_LAST | file path | "cache" |
| mrubyPostReadRequestFirst | ap_hook_post_read_request | APR_HOOK_FIRST | file path | "cache" |
| mrubyPostReadRequestMiddle | ap_hook_post_read_request | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyPostReadRequestLast | ap_hook_post_read_request | APR_HOOK_LAST | file path | "cache" |
| mrubyQuickHandlerFirst | ap_hook_quick_handler | APR_HOOK_FIRST | file path | "cache" |
| mrubyQuickHandlerMiddle | ap_hook_quick_handler | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyQuickHandlerLast | ap_hook_quick_handler | APR_HOOK_LAST | file path | "cache" |
| mrubyTranslateNameFirst | ap_hook_translate_name | APR_HOOK_FIRST | file path | "cache" |
| mrubyTranslateNameMiddle | ap_hook_translate_name | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyTranslateNameLast | ap_hook_translate_name | APR_HOOK_LAST | file path | "cache" |
| mrubyMapToStorageFirst | ap_hook_map_to_storage | APR_HOOK_FIRST | file path | "cache" |
| mrubyMapToStorageMiddle | ap_hook_map_to_storage | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyMapToStorageLast | ap_hook_map_to_storage | APR_HOOK_LAST | file path | "cache" |
| mrubyAccessCheckerFirst | ap_hook_access_checker | APR_HOOK_FIRST | file path | "cache" |
| mrubyAccessCheckerMiddle | ap_hook_access_checker | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyAccessCheckerLast | ap_hook_access_checker | APR_HOOK_LAST | file path | "cache" |
| mrubyCheckUserIdFirst | ap_hook_check_user_id | APR_HOOK_FIRST | file path | "cache" |
| mrubyCheckUserIdMiddle | ap_hook_check_user_id | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyCheckUserIdLast | ap_hook_check_user_id | APR_HOOK_LAST | file path | "cache" |
| mrubyAuthCheckerFirst | ap_hook_auth_checker | APR_HOOK_FIRST | file path | "cache" |
| mrubyAuthCheckerMiddle | ap_hook_auth_checker | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyAuthCheckerLast | ap_hook_auth_checker | APR_HOOK_LAST | file path | "cache" |
| mrubyFixupsFirst | ap_hook_fixups | APR_HOOK_FIRST | file path | "cache" |
| mrubyFixupsMiddle | ap_hook_fixups | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyFixupsLast | ap_hook_fixups | APR_HOOK_LAST | file path | "cache" |
| ここまでがレスポンス生成前 | ||||
| mrubyHandler | ap_hook_handler | APR_HOOK_REALLY_FIRST | file path | "cache" |
| mrubyHandlerFirst | ap_hook_handler | APR_HOOK_FIRST | file path | "cache" |
| mrubyHandlerMiddle | ap_hook_handler | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyHandlerLast | ap_hook_handler | APR_HOOK_LAST | file path | "cache" |
| mrubyInsertFilterFirst | ap_hook_insert_filter | APR_HOOK_FIRST | file path | "cache" |
| mrubyInsertFilterMiddle | ap_hook_insert_filter | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyInsertFilterLast | ap_hook_insert_filter | APR_HOOK_LAST | file path | "cache" |
| ここからがレスポンス送信後 | ||||
| mrubyLogTransactionFirst | ap_hook_log_transaction | APR_HOOK_FIRST | file path | "cache" |
| mrubyLogTransactionMiddle | ap_hook_log_transaction | APR_HOOK_MIDDLE | file path | "cache" |
| mrubyLogTransactionLast | ap_hook_log_transaction | APR_HOOK_LAST | file path | "cache" |
ちょっと例外のディレクティブ(今回は言及しません)
| Directive名 | C言語上のフックポイント名 | フック内での順番 | 引数1 | 引数2(optinal) |
|---|---|---|---|---|
| mrubyAuthnCheckPassword | 認証時 | - | file path | "cache" |
| mrubyAuthnGetRealmHash | 認証時 | - | file path | "cache" |
| mrubyOutputFilter | output filter用 | - | file path | "cache" |
ということで、上の表を参考にしつつ、レスポンス生成前後でどういうRubyスクリプトをフックさせて処理を実現するかを考えると良いと思います。それぞれのフックの用途は、Apacheモジュールの作り方等でググると大体でてくるので、ここでは説明しません。
入門としておさえておきたいディレクティブとその用途をまとめると、フックの順は上から順で、
-
mrubyPostConfigMiddleがmasterプロセスの初期化(設定ロード時) -
mrubyChildInitMiddleがworkerプロセスの初期化時 -
mrubyTranslateNameMiddleがリクエストのあったURLを解釈する時 -
mrubyMapToStorageMiddleが解釈したリクエストデータとサーバ上のデータ(ファイル等)と紐付ける時 -
mrubyAccessCheckerMiddleがリクエストを許可するかをアクセス制御する時 -
mrubyFixupsMiddleはレスポンスを返す直前で何かrewrite等の処理をしたりしないか最後の処理を行う時 -
mrubyHandlerMiddleはレスポンスを生成する時 -
mrubyLogTransactionMiddleはクライアントにレスポンスを送信した後の後処理時(ロギングや掃除等)
ぐらいを抑えておくと、それなりのApacheモジュールがRubyでかけると思います。
キャッシュオプション
ディレクティブの基本としてもう一つ忘れてはならないのが、キャッシュオプションになります。通常、以下のようにディレクティブを記述すると、
mrubyAccessCheckerMiddle /path/to/check.rb
リクエストのたびに、check.rbがコンパイルされます。つまり、毎回コンパイルのコストがかかってしまう分、性能が低下するデメリットがあります。
一方で、Apacheを再起動しなくても、check.rbのRubyコードを変更すれば、変更後の次のリクエストからは新たなコードの処理が走る事になります。そういう意味で、Apacheモジュールをリアルタイムで変更できるというメリットもあります。
ただし、より性能が要求される場面、例えば静的コンテンツへのリクエスト時に幾つかRubyでApacheモジュールを書いておきたいが内容自体は頻繁に変更しない、というような状況では以下のようにキャッシュオプションを渡すと良いです。
mrubyAccessCheckerMiddle /path/to/check.rb cache
これによって、check.rbはApache起動時にコンパイルされ、VMで実行される直前のバイトコードまでコンパイルされメモリに保存されます。そして、リクエスト時にバイトコードがフックされVM上でいきなり実行されます。
それによって、コンパイルのコストがかからず、比較的高速に動作します。一方で、皆さんお気づきだと思いますが、通常のApacheモジュールと同様、Apacheを再起動しないとcheck.rbの変更をApacheに反映させることはできません。
以上のように、mod_mrubyによってApacheモジュールをRubyで実装する際に、自分たちの問題意識において要求される状況はどういう状況かを考慮した上で、キャッシュオプションの使い分けると良いと思います。
クラスとメソッド入門
というようにディレクティブを理解すると、後はApache APIや構造体を操作するためにmod_mruby上で定義したRubyのクラスやメソッドを駆使してRubyコードを書き、それを渡したいフックポイントと紐付いたディレクティブ名に渡してやれば、ApacheモジュールがRubyでかけるはずですね。
ここでクラスやメソッドについて細かく語るとあまりに長くなってしまうので、細かいリファレンスはmod_mruby wiki Class and Methodをみてもらうとして、mod_mrubyで利用可能なクラスとメソッドについて、概要をここではお話します。
基本的な制御
Apacheのクラスメソッドを使って下さい。文字列をレスポンスで返すようなApache.echoだったり、リターンコードをセットしたり、ログ出力だったり、モジュールやサーバの情報を得ることができます。mod_mrubyの特に基本的なメソッドになるので、簡単に幾つか紹介します。
例えば、レスポンスを独自で出力したい場合は以下の様に書きます。
Apache.echo "hello world!"
また、上のサンプルと
Apache.rputs "hello world!¥n"
は一緒です。
また、HTTPリターンコードを返したい場合は以下のようにかけます。例えば503 HTTP_SERVICE_UNAVAILABLEを返したい場合は、
return Apache.return Apache::HTTP_SERVICE_UNAVAILABLE
または、
return Apache.return 503
です。
ロギングは以下のようになります。プライオリティはWikiを見て下さい。
Apache.errlogger Apache::APLOG_ERR, "mod_mruby error!"
Apache.syslogger Apache::LOG_ERR, "error occured!"
または、
Apache.log Apache::APLOG_ERR, "mod_mruby error!"
Apache.syslog Apache::LOG_ERR, "error occured!"
です。
クライアントからのリクエストを解析・操作
Apache::Requestクラスを使って下さい。コードを書く際には最も使うクラスになるでしょう。これは、URLのデータや紐付けるファイルのデータが入った構造体を操作するクラスとメソッドになります。ヘッダ情報やリクエストメソッド、プロトコルやリクエストbody、プロキシの設定等もこのクラスで操作可能です。サンプルは以下です。
r = Apache::Request.new
if /^.*¥.php$/ =~ r.filename
r.filename = "/path/to/redirect.php"
end
ただし、リクエストデータは各フックポイントにおいて、構造体に既に入っているものとそうでないものがあるので、例えば、Apache::Request.new.filenameにはどのフックのタイミングでデータが入るか等を考えながら実装すると良いと思います。(答えはmrubyMapToStrage***のタイミングです、それ以前のフックポイントでfilenameメソッドを呼び出した場合はnilが返ります)
あるフックポイントでメソッド実行してオブジェクトをとってみて、値がとれてないなぁと思った時は、もう少し後のフックポイントでメソッドを実行してみたりして下さい。この辺りはApacheの仕様になりますので、深い言及はここでは避けます。
後、リバースプロキシの操作もこのクラスを利用します。
# mrubyTranslateName*** ディレクティブでフック
r = Apache::Request.new
r.reverse_proxy "http://0.0.0.0:#{port}" + r.unparsed_uri
サーバの設定情報の操作
Apache::Serverクラスを使ってください。このクラスはサーバの設定、例えばホスト名だったりkeepaliveの設定だったり、ドキュメントルートや管理者情報、ログファイルに関する情報を操作する事ができます。
s = Apache::Server.new
Apache.echo s.hostname # => localhost.localdomain
クライアントのコネクション情報の取得
Apache::Connectionクラスを使って下さい。接続元IPやポート、サーバIPやポート、keepaliveの有無等が得られます。
c = Apache::Connection.new
# curl http://127.0.0.1/hello?a=1
Apache.echo c.remote_ip # => 127.0.0.1
アクセス頻度や負荷情報の取得
Apache::Scoreboardクラスを使って下さい。Apache起動後のuptimeや総アクセス数、総転送料、ロードアベレージ、idleワーカー数、busyワーカー数等が得られます。この値を利用して、アクセス制限や動的なリバースプロキシ等を実現すると面白いでしょう。
リクエストファイルの情報取得
Apache::Finfoクラスを使って下さい。以下のようなリクエストファイルのstat情報がとれます。
r = Apache::Request.new
finfo = Apache::Finfo.new
Apache.echo "---- finfo ----"
Apache.echo "Request filename: #{r.filename}"
Apache.echo "permission: #{finfo.permission}"
Apache.echo "filetype: #{finfo.filetype}"
Apache.echo "group: #{finfo.group}"
Apache.echo "user: #{finfo.user}"
Apache.echo "device: #{finfo.device}"
Apache.echo "inode: #{finfo.inode}"
Apache.echo "nlink: #{finfo.nlink}"
Apache.echo "size: #{finfo.size}"
Apache.echo "csize: #{finfo.csize}"
Apache.echo "atime: #{Time.at(finfo.atime/1000000)}"
Apache.echo "ctime: #{Time.at(finfo.ctime/1000000)}"
Apache.echo "mtime: #{Time.at(finfo.mtime/1000000)}"
Apache.echo "---- Request File Respnse ----"
環境変数の操作
Apache::Envクラスを使って下さい。CGIでお馴染みの環境変数等が得られたり、環境変数を操作することもできます。
e = Apache::Env.new
# curl http://localhost/hello?a=1
Apache.echo "--- env ----"
e.all.keys.each do |key|
Apache.echo "#{key}: #{e[key]}"
end
Noteの利用
Apache::Noteクラスを使って下さい。Apacheのnote機能は、あるリクエストがレスポンスを返す間に各フックポイントでデータを記録し、別のフックポイントで取り出す事ができる機能です。それをハッシュで制御できるようにしたので、同一リクエスト間のみ有効なデータを各フック間で安全にハッシュのやりとりすることができます。
note = Apache::Notes.new
note["This_is"] = "OK"
としておいて、別のフックポイント(同一のリクエスト範囲内)
note = Apache::Notes.new
if note["This_is"] == "OK"
# any implementaton...
end
のように使えます。
その他のクラス
その他、mod_mrubyはデフォルトで幾つかのmrbgem(mrubyの外部モジュール)をリンクするようにしています。そのmrbgemはbuild_config.rb内で確認できます。そらのmrbgemのクラスももちろんmod_mrubyのフックスクリプトで使えますし、他にリンクさせたいmrbgemがあれば、build_config.rbに追記して、再度ビルドすると使えるようになります。
#
# Recommended for mod_mruby
#
conf.gem :github => 'iij/mruby-io'
conf.gem :github => 'iij/mruby-process'
conf.gem :github => 'iij/mruby-pack'
conf.gem :github => 'iij/mruby-env'
conf.gem :github => 'iij/mruby-dir'
conf.gem :github => 'iij/mruby-digest'
conf.gem :github => 'mattn/mruby-json'
conf.gem :github => 'matsumoto-r/mruby-redis'
conf.gem :github => 'matsumoto-r/mruby-vedis'
# conf.gem :github => 'matsumoto-r/mruby-memcached'
conf.gem :github => 'matsumoto-r/mruby-sleep'
conf.gem :github => 'matsumoto-r/mruby-userdata'
conf.gem :github => 'matsumoto-r/mruby-uname'
conf.gem :github => 'mattn/mruby-onig-regexp'
# mod_mruby extended class
conf.gem :github => 'matsumoto-r/mruby-mod-mruby-ext'
# use markdown on mod_mruby
# conf.gem :github => 'matsumoto-r/mruby-discount'
# Linux only for mod_mruby
# conf.gem :github => 'matsumoto-r/mruby-capability'
# conf.gem :github => 'matsumoto-r/mruby-cgroup'
というように、これらのリファレンスはmod_mruby wiki Class and Methodに載っているので、参考にして頂ければと思います。また、間違いや足りてない箇所があれば自由に追記して頂いて結構です。
まとめ
以上のように、今回はmod_mrubyインストール後入門ということで、mod_mrubyはRubyでApacheモジュールを書くものであり、実装の際にApacheのフックポイントを意識しながら、そこにRubyコードを渡し、コード内にはApache内部を制御するメソッドを書く、という流れを説明しました。
これを頭のなかで意識しながら、「このフックポイントでこうなったら500のコードを返そう」みたいな事を考えつつ書くと、mod_mrubyの知識はもちろんのこと、Apacheの理解も深まるかと思います。
その他サンプルは、mod_mrubyレポジトリのexampleやWikiのユースケースに随時書いていきますので、参考にしてみてください。後はテストの実装もさんこうになるかもしれません。
後は、認証やアウトプットフィルターという、上記のディレクティブの書き方は違った例外的な書き方があるのですが、それは今回は混乱をさけるために省略したいと思います。また別の機会で説明したいと思います。
上記の考え方と流れを頭に入れておくと、それなりに色々とRubyでApacheモジュールを書く事ができるのではないでしょうか。
ということで、明日は再び @hkusu さんの「mod_mrubyの小ネタ集」です。小ネタは大好きなので非常に楽しみです!
