でこてっくろぐ ねお

UbieのSRE。でこらいふろぐ(http://dekolife.hatenablog.com/)の姉妹版。デコテックログ(deko tech log)である

nginxでproxy_hide_header, proxy_set_header, add_headerを書く時にはまりがちな罠

reverse proxyサーバとしてよく使われているnginxですが、意外にハマりやすい罠があったりします。

今回は、proxy_hide_header, proxy_set_header, add_header等で設定内容を複数コンテキストに分割する際にはまりがちな点を紹介します。

概要

  • 上記3つのディレクティブについては、基本的には現在のコンテキストにそのディレクティブがない時に限って上位のコンテキストで設定された値が継承される
    • 他にもaccess_logディレクティブなどでも同様の動きをするようだが、まだどのディレクティブがそのように動くかの一覧は出せていない

どのようなときにハマるのかストーリー

上記説明だと、具体的に何がどうなるのかがわかりづらいため、ストーリー仕立てで説明します。 nginx - app のように、リバースプロキシの後ろにアプリケーションサーバがいるような一般的な形を考えます。

このアプリケーションサーバには、nginxでHTTPヘッダを付与する必要があったとします。今回のストーリでは以下のようにX-HogeとX-Fugaヘッダを付与します。

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
}

さて、この状態は特に問題ありません。アプリケーションサーバでX-HogeとX-Fugaの値をロギングするようにした状態でnginxにアクセスを行うと、ログから以下のように正しく値がセットされていることが分かります。

X-Hoge:hogevalue        X-Fuga:fugavalue

ここで、やんごとなき事情によって/test/以下へのアクセスのみ、X-Hoge, X-Fuga以外に、X-Piyoヘッダも付与する必要が出てきたとします。直感的に以下のように設定を変更すれば良さそうな感じがします

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_set_header X-Piyo piyovalue; # これで X-Hoge, X-Fuga, X-Piyoが設定されるはず!
        proxy_pass http://app;
    }
}

うまく動きそうですね。/test/にきたら、X-HogeもX-FugaもX-Piyoも付与されていることが期待できます。では、/test/以下にアクセスしてみます。

$ curl http://nginx/test/index.html

ログは以下の通り

X-Hoge:-        X-Fuga:-        X-Piyo:piyovalue

…X-HogeとX-Fugaが設定されていませんね…

この場合は、以下のように必要な設定を全部入れてあげる必要があります。

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_set_header X-Hoge hogevalue; # ここにも書く
        proxy_set_header X-Fuga fugavalue; # ここにも書く
        proxy_set_header X-Piyo piyovalue;
        proxy_pass http://app;
    }
}

そうすると当然ながら以下のように必要なヘッダが全て渡っていることが分かります。

X-Hoge:hogevalue        X-Fuga:fugavalue        X-Piyo:piyovalue

同様の動きをする他のヘッダ達

上記ストーリーではproxy_set_headerを例に出しましたが、proxy_hide_header, add_header等他にも同様の動きをするディレクティブが存在します(が、詳しくは追えていない。ドキュメントには一覧はなさそう?)。 以下にも記載しましたが、ディレクティブによっては、そのディレクティブの説明でこのような動きをする旨記載してあるものもあります。

ドキュメント上ではどのように扱われているのか

proxy_set_headerやadd_headerのドキュメントには、以下のような記述がありました。

These directives are inherited from the previous level if and only if there are no proxy_set_header directives defined on the current level.

Module ngx_http_proxy_module

ただ、proxy_hide_headerのドキュメントにはなさそう?

more_set_headersはまた違う動きをする

openrestyのheaders-more-nginx-moduleを使うと、add_headerをもう少し強力にしたようなディレクティブとしてmore_set_headersを使うことができるようになります。 add_headerと同様にクライアントに返すHTTPヘッダを追加することができるディレクティブなのですが、add_headerとの違いは、add_headerは既に存在するHTTPヘッダを上書きすることはできませんが、more_set_headersを使うと上書きすることができます。

ただ、それ以外に、add_headersとは、この記事で書いている挙動をするかしないかという点で、異なる動きをします。

以下で、実際にどのように異なる動きをするのかを解説します。

まず、以下のようにadd_headerとmore_set_headersでそれぞれヘッダを付与していたとします。

server {
    listen       80;
    server_name  nginx;

    add_header X-Add-Header-1 add-header-1;
    add_header X-Add-Header-2 add-header-2;
    more_set_headers 'X-More-Set-Headers-1: more-set-headers-1';
    more_set_headers 'X-More-Set-Headers-2: more-set-headers-2';

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

curlでリクエストを投げると以下のようにどちらで設定したヘッダも出力されています(なお、curlのオプションの-Iというのはヘッダを出力するオプションです。ただ、-Iを付与するとGETではなくHEADリクエストになってしまうのでそれを阻止するために-XGETオプションも一緒に指定しています。)

$ curl -IXGET http://nginx/test/index.html
HTTP/1.1 200 OK
Server: openresty/1.9.7.4
Date: Sun, 22 May 2016 13:38:14 GMT
Content-Type: text/html
Content-Length: 12
Connection: keep-alive
Last-Modified: Sun, 22 May 2016 12:52:29 GMT
ETag: "5741ab8d-c"
Accept-Ranges: bytes
X-More-Set-Headers-1: more-set-headers-1
X-More-Set-Headers-2: more-set-headers-2
X-Add-Header-1: add-header-1
X-Add-Header-2: add-header-2

ここで、以下のように、/test/以下にアクセスがあった場合にさらにいろいろヘッダを付与するような設定をしたとします。

server {
    listen       80;
    server_name  nginx;

    add_header X-Add-Header-1 add-header-1;
    add_header X-Add-Header-2 add-header-2;
    more_set_headers 'X-More-Set-Headers-1: more-set-headers-1';
    more_set_headers 'X-More-Set-Headers-2: more-set-headers-2';

    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_pass http://app;
        add_header X-Add-Header-3 add-Header-3;
        more_set_headers 'X-More-Set-Headers-3: more-set-headers-3';
    }
}

すると、curlの結果は、以下のように、more_set_headersで付与したHTTPヘッダはそのまま上位のコンテキストで設定されたものに下位のコンテキストで設定した結果を付け足していくような動きになっていますが、add_headerは、上記で説明したproxy_set_headerと同様に、上位のコンテキストで設定したHTTPヘッダは付与されていません。難しいですね。確実に、両方同時に使わない方が良いです。

$ curl -IGET http://nginx/test/index.html
HTTP/1.1 200 OK
Server: openresty/1.9.7.4
Date: Sun, 22 May 2016 13:40:47 GMT
Content-Type: text/html
Content-Length: 12
Connection: keep-alive
Last-Modified: Sun, 22 May 2016 12:52:29 GMT
ETag: "5741ab8d-c"
Accept-Ranges: bytes
X-More-Set-Headers-1: more-set-headers-1
X-More-Set-Headers-2: more-set-headers-2
X-More-Set-Headers-3: more-set-headers-3
X-Add-Header-3: add-Header-3

あるディレクティブがそのように動くかどうかどのように調べればいいか

ドキュメントを読む、実際に試験をしてみる、実装を読むしかなさそうです。 まだ実装を読んでないですがまた読んだらブログにまとめたいです。

あとがき

最近nginxをよく触っているのですが、ここで紹介したproxy_hide_header, proxy_set_header, add_headerの罠に私は既に2回ハマっております。恥ずかしい。また周りでもちょくちょくハマっている姿が見られるため、そのような不幸な人が1人でも減ることを願って、この記事を執筆しました。

よいnginxライフを!

この記事によって今後書きたくなった記事

  • nginxのドキュメントへのコントリビュート
    • hgでコントリビュートするらしいというところまで見て今回は土日が終わってタイムアウト
  • ディレクティブの実装がどのような場合に上記のような動きをするがについてまとめる