[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

nginxでクエリストリングに応じて静的ファイルをばらまきたかった

そろそろISUCONということで、先日チームであつまって練習をしたところnginxでドハマリしたので復習した内容をまとめます。

やりたかったこと

ISUCONはほとんどがフロントにWEBサーバ(nginx)があり、裏にgunicorn等のAPサーバを配置した構成が多いようです。デフォルトはすべてのリクエストがAPサーバに流れているので一部の静的なページをnginxに任せたかったわけです。

その時の条件は以下でした。

  • /へのアクセスはnginxに静的ファルを戻させる
  • /xxx へのアクセス等、/以外はすべてgunicornにに引き渡す
  • /?error=xの場合はxの値に応じた静的ファイルをnginxに戻させる

ネックになったのはgunicornに引き渡すルールが/appみたいになっておらず同じ/でも適用するディレクィブをわけなければいけなかった点です。

nginxの内部リダイレクト

nginxではindexディレクティブ等を使った場合、内部リダイレクトが発生します。以下を例とします。

    # ディレクティブ1
    location = / {
      add_header Content-type text/html;
      default_type text/html;
      index index.html;
      root /tmp/test/;
    }
    # ディレクティブ2
    location / {
      proxy_pass http://app;
    }

気持ち的には、ディレクティブ1が適用され、/tmp/test/index.htmlが戻されてほしいですが、実際は以下の流れになります。

  1. ディレクティブ1が適用される
  2. indexディレクティブにより/index.htmlへアクセスが書き換えられる
  3. 内部リダイレクトが発生し、/index.htmlとして再評価される
  4. ディレクティブ1にはマッチしないのでディレクティブ2にマッチし、APサーバに流れる

なお、/tmp/test/index.htmlが実在しない場合は403が戻りますのでさらに混乱します。

root@denzow-ubuntu:/etc/nginx# ls -l /tmp/test/index.html
-rwxr-xr-x 1 root root 23 10月  4 22:59 /tmp/test/index.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/
i am bottle <br/> index.html -> APサーバが応答している
root@denzow-ubuntu:/etc/nginx# rm /tmp/test/index.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.10.3 (Ubuntu)</center>
</body>
</html>  -> ファイルを削除すると403になる

結局、ファイルがあるかどうかは最初のlocationディレクティブでチェックし、そこで403が戻っているようです。

nginxでのクエリパラメータによる分岐

nginxではhoge?a=1&b=2のようなクエリパラメータの場合は$arg_a$arg_bとして取得することができます。これにifディレクティブを組み合わせると以下のようになります。

        if ( $arg_error = "1" ) {
            #任意のアクション
        }
        if ( $arg_error = "2" ) { 
            #任意のアクション
        }

なお、if内ではalias 等使えないディレクティブもあるようです。

結局どうしたか

rewriteディレクティブを使うことにしました。このディレクティブはアクセスされたURLを書き換えるものですが、更にlocationの再評価をさせるかどうかを指定することが可能です。

rewrite ^(.*)$ /error1.html break;

このように末尾にbreakをつけ場合はlocationの再評価をしないため、ここでアクセス先が確定します。これを利用し以下の様に設定しました。

:
  upstream app {
    server localhost:8080;
  }

  server {

    location = / {
        add_header Content-type text/html;
        default_type text/html;
        root /tmp/test/;
        
        if ( $arg_error = "1" ) {
            rewrite ^(.*)$ /error1.html break;
        }
        if ( $arg_error = "2" ) { 
            rewrite ^(.*)$ /error2.html break;
        }
        
        rewrite ^(.*)$ /index1.html break;
     
    }

    location / {
      proxy_pass http://app;
    }
  }
:

これで、以下のような動きになります。

URL アクセス先
/ /tmp/test/index1.html
/?error=1 /tmp/test/error1.html
/?error=2 /tmp/test/error2.html
/hoge apサーバ宛
/foo apサーバ宛

確認してみましょう。

root@denzow-ubuntu:/etc/nginx# ls -l /tmp/test/
合計 12
-rwxrwxrwx 1 root root 24 10月  4 22:44 error1.html
-rwxr-xr-x 1 root root 24 10月  4 22:44 error2.html
-rwxrwxrwx 1 root root 24 10月  4 23:11 index1.html
root@denzow-ubuntu:/etc/nginx# grep . /tmp/test/*
/tmp/test/error1.html:static file error1.html
/tmp/test/error2.html:static file error2.html
/tmp/test/index1.html:static file index1.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/
static file index1.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/?error=1
static file error1.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/?error=2
static file error2.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/hoge
i am bottle <br/> hoge 
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/foo
i am bottle <br/> foo 

ちゃんと思った通りの動きになっていますね。

なお、nginxのifディレクティブは実運用では推奨されていないようです。あくまでISUCON等の限られた場で生きる知識として理解したいと思います。