RackがよくわからなかったのでRackアプリケーションをUnicornで動かしてnginxからリクエストを転送してみた
この記事はRuby Advent Calendar 2017の23日目の記事です。
はじめに
Webアプリケーションフレームワーク(WAF)といえば、
- 薄いアプリケーションをサクッと書くのに適しているsinatra
- なんでもあり気味なRuby on Rails
など、rubyで有名なものだけでもぱっと複数名前が上がりますね。
これらのWAFはApacheやnginxと連携させて動作させますが、これらのWebサーバはそのままではWAFと連携して動きません。なにかしら間にコードを書く必要があるのですが、このコードのインタフェースがWAFによって違ったらWAFを選んだら連携させるサーバも固定されてしまいますね。
そこで、WAFとWebサーバの間にこれらが協調動作するためのインタフェースを設定しましょう、それがrack...
とまで書いていてやっぱり意味がよくわかっていないので、この記事ではnginxとRackアプリケーションをつないで動かしてみようと思います。
実行環境について
この記事で利用しているバージョンは
nginxをインストールする
http://nginx.org/en/download.html よりnginx 1.13.7をダウンロードします。
$ cd /path/to/nginx-1.13.7 $ ./configure ... 中略 ... ./configure: error: the HTTP rewrite module requires the PCRE library. You can either disable the module by using --without-http_rewrite_module option, or install the PCRE library into the system, or build the PCRE library statically from the source with nginx by using --with-pcre=<path> option.
と出たので、こちらの記事を参考にpcreを入れます。
$ cd /path/to/pcre $ curl --remote-name ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.41.tar.gz $ tar -xzvf pcre-8.41.tar.gz
pcreはURLのrewirteやLocationの指定で正規表現によるマッチングが行えるようにしてくれるそうです。 つまり、pcreがあれば、ディレクトリにファイルをおいた場合、そのパスにアクセスした際にファイルを返してくれるのかなと一瞬思ったので、pcreをはずしてnginxをビルドしてみました。
$ cd /path/to/nginx-1.13.7 # 試しに --without-http_rewrite_module オプションでやってみる $ ./configure --without-http_rewrite_module $ make $ sudo make install $ ls /usr/local/nginx/sbin nginx $ ls /usr/local/nginx conf html logs sbin $ /usr/local/nginx/sbin/nginx nginx: [alert] could not open error log file: open() "/usr/local/nginx/logs/error.log" failed (13: Permission denied) 2017/12/23 01:29:52 [emerg] 7844#0: mkdir() "/usr/local/nginx/client_body_temp" failed (13: Permission denied) $ sudo /usr/local/nginx/sbin/nginx $ ps aux | grep nginx woshidan 7900 0.0 0.0 2443044 828 s003 S+ 1:31AM 0:00.00 grep nginx nobody 7892 0.0 0.0 2456480 1040 ?? S 1:31AM 0:00.00 nginx: worker process root 7891 0.0 0.0 2455228 484 ?? Ss 1:31AM 0:00.00 nginx: master process /usr/local/nginx/sbin/nginx $ sudo /usr/local/nginx/sbin/nginx -s stop $ ps aux | grep nginx woshidan 7917 0.0 0.0 2443044 824 s003 S+ 1:31AM 0:00.00 grep nginx
sudo /usr/local/nginx/sbin/nginx
で http://localhost
にアクセスすると index.html が見れます。
そして、 http://localhost/50x.html
にアクセスするとちゃんとエラーページが表示されます。違ったみたいですね。
この記事のごくせまい範囲では問題ありませんでしたが多分正しくないと思います。ただ、今回はnginxの話がメインでないので、pcreの話はまた改めてじっくり調べます。。
最初のRackアプリケーションを書く
rackのインタフェースでWAFとWebサーバを協調させるとき、WAFの方は Rackアプリケーション
と呼ばれるのですが、Rackアプリケーションとは何かというと
- callというメソッドを持つ
- callメソッドの引数としてWebサーバからのリクエストに当たるオブジェクトを一個受け取る
- callメソッドは、次の要素を含むレスポンスにあたる配列を返す
- ステータスコード(Integer)
- レスポンスヘッダ(Hash)
- レスポンスボディ(Array)
というRubyプログラムのことみたいです。
http://gihyo.jp/dev/serial/01/ruby/0023 の記事を参考に単純なラックアプリケーションを書いてみます。
GETリクエストにのみ反応し、URLパラメータを含むJSONを返してくれるアプリケーションです。
# test_app.rb # config: utf-8 class TestApp def call(env) if env['REQUEST_METHOD'] == 'GET' [ 200, {'Content-Type' => 'application/json'}, ["{ \"your_input\": \#{env['QUERY_STRING']}"\" }"] ] end end end
test_app.rb
と同じディレクトリに以下のように config.ru
というファイルも用意して
# config.ru # coding: utf-8 require './test_app.rb' run TestApp.new
$ ls
... config.ru test_app.rb
$ ./rackup
とすると
$ rackup [2017-12-23 01:48:38] INFO WEBrick 1.3.1 [2017-12-23 01:48:38] INFO ruby 2.4.3 (2017-12-14) [x86_64-darwin16] [2017-12-23 01:48:38] INFO WEBrick::HTTPServer#start: pid=8095 port=9292
のようにどこかでよくみたWEBrickのログが現れます。
http://localhost:9292
にアクセスすると { "your_input": "" }
というJSONが表示され、 http://localhost:9292?test=parameter
のようにクエリパラメータをつけると { "your_input": "parameter=test" }
というJSONに変化します。
雑にもほどがありますが、一応動いてそうですね。
nginxとRackアプリケーションはこのままでは一緒に動作しませんが、これらをつなぐものとして Rackアプリケーション用サーバ
があり、Rackアプリケーション用サーバ
には Unicorn
などがあるらしいです。今回は Unicorn
をさわってみます。
UnicornとRackアプリケーション
http://w.koshigoe.jp/study/?ruby-unicorn-intro より引用すると
Unicornは、Unix系システムで動作するRackアプリケーション用サーバ。接続時間が短いことを前提とした設計となっている。
だそうです。書いてあったことで今回の作業に必要かもしれなさそうなことをざっくりまとめます。
- マスタプロセスが規定の数のワーカープロセスが稼働しているのを維持する
- Rackアプリケーションをロードする((Rackアプリケーションは少なくとも
Rackアプリケーション用サーバ
とは別のプロセスで動いてないんですね)) - 設定ファイルをRuby DSLで書く
- Unicornのプロセスを止めたり増やしたりするのはマスタープロセスにシグナルを送ることによって行う
アプリケーションのpreloadもできるそうですが、一旦先に進みます。ひとまず unicorn
のgemを入れます。
$ sudo gem install unicorn
unicornを動作させる前にいくつか必要なディレクトリを用意します。
mkdir tmp mkdir tmp/sockets mkdir tmp/pids mkdir log
こちらの記事やこちらの記事を元に、可能な限り少なく設定ファイル unicorn.rb
を書いてみます。
worker_processes 2 # 子プロセスいくつ立ち上げるか timeout 15 # Unicornのソケットが開くパスを指定 # この設定をnginx.confでも使います listen "#{@dir}tmp/sockets/unicorn.sock", :backlog => 64 # ログのパスを指定 stderr_path "#{@dir}log/unicorn.stderr.log" stdout_path "#{@dir}log/unicorn.stdout.log"
最低限の設定ファイルはこれだけで、
# ファイルの配置を確認 $ tree . . ├── config.ru # Rackアプリケーションの起動設定ファイル ├── log ├── test_app.rb # Rackアプリケーションの実装 ├── tmp │ ├── pids │ └── sockets └── unicorn.rb # Unicornの設定ファイル
上記を用意した上で unicorn
と入力すると、
$ unicorn I, [2017-12-23T16:09:57.684313 #11150] INFO -- : listening on addr=0.0.0.0:8080 fd=9 I, [2017-12-23T16:09:57.684439 #11150] INFO -- : worker=0 spawning... I, [2017-12-23T16:09:57.685355 #11150] INFO -- : master process ready I, [2017-12-23T16:09:57.686091 #11163] INFO -- : worker=0 spawned pid=11163 I, [2017-12-23T16:09:57.686483 #11163] INFO -- : Refreshing Gem list I, [2017-12-23T16:09:57.706864 #11163] INFO -- : worker=0 ready
見覚えのあるログを吐くプロセスが立ち上がりました。ブラウザで http://localhost:8080/?parameter=test
にアクセスすると先ほどのRackアプリケーションと同じJSONを返してもらえます。
実際動かすまでは、UnicornのプロセスからRackアプリケーションのプロセスにリクエストを接続するのかなーと思っていたのですが、Rackアプリケーションを unicorn
コマンドで起動した場合と rackup
コマンドで起動した場合とのプロセスを確認してみたところ、確かに Unicorn
のプロセスしか動いてなくて Unicorn
のプロセスでRackアプリケーションが動いている感じがしてきました。
# unicornから動かした場合 $ ps aux | grep unicorn woshidan 11267 0.0 0.0 2442020 816 s002 S+ 4:14PM 0:00.00 grep unicorn woshidan 11163 0.0 0.1 2467944 8392 s003 S+ 4:09PM 0:00.02 unicorn worker[0] -l0.0.0.0:8080 woshidan 11150 0.0 0.1 2467900 10724 s003 S+ 4:09PM 0:00.11 unicorn master -l0.0.0.0:8080 $ ps aux | grep rack # 何も出ない # rackupで動かした場合 $ ps aux | grep rack woshidan 11335 0.0 0.0 2442020 812 s000 S+ 4:14PM 0:00.00 grep rack woshidan 11297 0.0 0.1 2480696 11732 s003 S+ 4:14PM 0:00.13 /Users/woshidan/.rbenv/versions/2.4.3/bin/ruby /Users/woshidan/.rbenv/versions/2.4.3/bin/racku
nginxとUnicorn
さて、今回のテーマは「nginxとRackアプリケーションをつないでみる」だったので、Rackアプリケーション
を動かしている Unicorn
と nginx
を連携させるための設定もしてみます。
nginx.confの書き方については
http
httpモジュールの設定server
仮想サーバごとの設定を書きます*1upstream server_name
server
ディレクティブの中でproxy_pass server_name
と書かれていた場合、upstream
ディレクティブの中のサーバに転送する- ロードバランサとしての負荷分散みたいな設定をするところっぽい
と上記の内容だけ確認して書いた最小限の設定ファイルが以下となっていて
// nginx.conf #user nobody; worker_processes 1; #pid logs/nginx.pid; events { worker_connections 4; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 15; upstream test-app { server 127.0.0.1:8080; # ローカルでunicornが動いてるIPアドレス & ポートへ転送 } server { listen 80; server_name localhost; location / { root html; # /usr/local/nginx からのパスです try_files $uri @app; } location @app { proxy_pass http://test-app; } } }
設定内容をざっくりまとめると
- localhost(ポート番号80番) でHTTPリクエストを受け付けるHTTPサーバ
/
のパスへアクセスされた時はまず/usr/local/nginx/html
(ドキュメントルート)以下の対応するパスにファイルがあるか確認する- ファイルがあればファイルの内容を返す
- ファイルがなければRackアプリケーションへ転送する
- Rackアプリケーションは同じローカルマシンで動いていてポート番号 8080番にリクエストを投げれば良い
となります。
この内容を、元ファイルのバックアップを取った上で /usr/local/nginx/conf/nginx.conf
に上書きしてから nginx
を起動します。
$ sudo /usr/local/nginx/sbin/nginx
すると、ドキュメントルートの usr/local/nginx/html
から辿って静的ファイルのあるパスの /index.html
や /50x.html
に対してはhtmlをそれ以外の場合は先ほどのRackアプリケーションが処理したJSONを返してくれます。
nginx
がパスを見て静的ファイルを返すかアプリケーションへリクエストを転送するかといった判断をしてくれているようで、それっぽくていいですね。
まとめ
ここまで動かしてみてRackでWebサーバとWAFを連携させるとかいうのは多分この辺りのことを言ってるんだろうな、と思ったことをメモします。
- Rackアプリケーションとは何かしらの引数を一つ取る
call
メソッドを持ったRubyのプログラムのことでRailsやSinatraもRackアプリケーションである - rackインタフェースないしRack protocolはその
call
メソッドで受け取るオブジェクトやcall
メソッドの戻り値などを規定している - HTTPリクエストを受け取ってrackインタフェースにそったオブジェクトを
Rack
アプリケーションに渡してくれるWebサーバを Rackアプリケーション用サーバという - 一般的にはリクエストを受け取ったらすべてそのままWebアプリケーションに流したいわけではなく静的ページに流したりロードバランサ挟んだりしたいので通常のWebサーバをRackアプリケーション用サーバ の前段に置く
- nginxやApacheあたりのこと
- nginxの設定で、特定のパスへのリクエストを
Rackアプリケーション用サーバ
に転送する- 転送の際にロードバランサの設定を挟んだり、特定のページへは静的ファイルを返すようにしたりする
まだまだ気になることは残っているのですが、十分長くなったのでおかわり案件にして現場からは以上です。
参考
- unicornの話
- unicorn/nginx/rackアプリケーションをつなぐ話
- rackの話
- nginx
- 導入
- コマンド
- 設定
- https://qiita.com/syou007/items/3e2d410bbe65a364b603
- https://heartbeats.jp/hbblog/2012/02/nginx03.html
- https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967#%E5%9F%BA%E6%9C%AC%E8%A8%AD%E5%AE%9A
- http://nginx.org/en/docs/beginners_guide.html ファイルのパスなど一番正確なのは公式ですね...
- https://qiita.com/kaikusakari/items/cc5955a57b74d5937fd8 try_files の件
- http://shim0mura.hatenadiary.jp/entry/20120110/1326198429 upstreamの件
*1:仮想サーバについては http://www.sakc.jp/blog/archives/41325