woshidan's blog

そんなことよりコードにダイブ。

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アプリケーションをつないで動かしてみようと思います。

実行環境について

この記事で利用しているバージョンは

  • ruby 2.4.3
  • rack 2.0.3
  • nginx 1.13.7
  • unicorn 5.3.1 (gemのバージョン)

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/nginxhttp://localhost にアクセスすると index.html が見れます。 そして、 http://localhost/50x.html にアクセスするとちゃんとエラーページが表示されます。違ったみたいですね。

この記事のごくせまい範囲では問題ありませんでしたが多分正しくないと思います。ただ、今回はnginxの話がメインでないので、pcreの話はまた改めてじっくり調べます。。

最初のRackアプリケーションを書く

rackのインタフェースでWAFとWebサーバを協調させるとき、WAFの方は Rackアプリケーション と呼ばれるのですが、Rackアプリケーションとは何かというと

  • callというメソッドを持つ
  • callメソッドの引数としてWebサーバからのリクエストに当たるオブジェクトを一個受け取る
  • callメソッドは、次の要素を含むレスポンスにあたる配列を返す

という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で書く
    • フックは3種類あって
      • before_exec ... reexecによって新しいマスタプロセスを作る際のexec()実行の直前
      • before_fork ... ワーカープロセスをfork()する直前。
      • after_fork ... ワーカープロセスをfork()した後、いくつかの内部処理を終えた後。
  • Unicornのプロセスを止めたり増やしたりするのはマスタープロセスにシグナルを送ることによって行う
    • 一部の分かりやすいシグナルだけ抜粋してメモ
      • QUIT ... 処理中のワーカープロセスの処理の完了を待ってから終了させる。
      • TTIN ... ワーカープロセスを一つ増やす。
      • TTOU ... ワーカープロセスを一つ減らす。

アプリケーションの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アプリケーション を動かしている Unicornnginx を連携させるための設定もしてみます。

nginx.confの書き方については

  • http httpモジュールの設定
    • listenするポートやどのIPアドレスの設定
    • 外部からアクセスするときのドメイン名は server_name?
    • root ドキュメントルート((本当に外部にrootディレクトリを公開すると危ないので http://ドメイン/ でアクセスされた時に公開するディレクトリを指定する。ドキュメントルートにindex.htmlを用意しておくとWebサーバはトップページとして表示しようとする 参考: ドキュメントルート))の設定
  • server 仮想サーバごとの設定を書きます*1
    • location PATH ... URIごとに設定が変わる場合はそれを記述します
      • try_files $uri @app ... $uri = URLのパスにファイルがあるか、なければ @app のlocationで指定した設定が適用される
  • upstream 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のプログラムのことでRailsSinatraもRackアプリケーションである
  • rackインタフェースないしRack protocolはその call メソッドで受け取るオブジェクトや call メソッドの戻り値などを規定している
  • HTTPリクエストを受け取ってrackインタフェースにそったオブジェクトを Rack アプリケーションに渡してくれるWebサーバを Rackアプリケーション用サーバという
    • Phusion PassengerのnginxやApacheに対して拡張している部分やUnicornなどがここに該当する
  • 一般的にはリクエストを受け取ったらすべてそのままWebアプリケーションに流したいわけではなく静的ページに流したりロードバランサ挟んだりしたいので通常のWebサーバをRackアプリケーション用サーバ の前段に置く
    • nginxやApacheあたりのこと
  • nginxの設定で、特定のパスへのリクエストを Rackアプリケーション用サーバ に転送する
    • 転送の際にロードバランサの設定を挟んだり、特定のページへは静的ファイルを返すようにしたりする

まだまだ気になることは残っているのですが、十分長くなったのでおかわり案件にして現場からは以上です。

参考

*1:仮想サーバについては http://www.sakc.jp/blog/archives/41325