要約
- proxy_redirect ディレクティブでレスポンス中の絶対パスは書き換えられない
- proxy_redirect の書き方はプロキシ転送先のサービス次第
- sub_filter ディレクティブで絶対パスを書き換える
- sub_filter ディレクティブを効かすには Accept-Encoding をキャンセルする
背景
コンテキストパスなしのWebサービスは多い。そもそも、コンテキストパス非対応のWebアプリも少なくない。(メジャーなWebアプリ以外は非対応と思った方がいい。)
社内だとサービスごとにWebサーバーが構築され、次のいずれかのURL形式でサービス提供されていることも多いはず。
ID | URL形式 | 説明 |
---|---|---|
(A) | http(s)://host-name/ |
ホスト名のままでサービス提供 |
(B) | http(s)://host-name:9000/ |
ポート番号を変えて1サーバーで複数サービスを提供 |
(C) | http://service-cname/ |
社内 DNS に CNAME レコードを登録してサービス提供 |
仮想化が当たり前の昨今だが、上記 (A) のようにサービスごとにVMを構築するのは無駄が多い。そこで、docker エンジンを導入してコンテナで各サービスを提供したりするだろう。
その場合、(B) が一番簡単。SSL証明書も1つで済む。でも、ポート番号が増えると覚えられないし、セキュリティ面から利用するポート番号は最小限に制限しておきたい。
(C) は許可するポート番号を増やさずに済むが、ネットワーク管理者にDNSレコードを登録してもらう手間が掛かるし、SSL対応も面倒。
そこで、リバースプロキシを導入して https://reverse-proxy.your-domain.com/context-path/
のようにコンテキストパスで複数サービスを提供したくなる。利用するポート番号は増えず、SSL証明書も1つで済む。
このように、リバースプロキシとコンテキストパスを併用したWebサービス提供が理想形なのだが、そのリバースプロキシ設定にはいろいろな技が必要になる。
よくある課題
リバースプロキシには nginx の出番となるが、ネットの情報を頼りに location
と proxy_path
ディレクティブを設定しても、それだけではうまくいかない場合が多い。
よくある問題現象として、css スタイルが適用されていない文字だけの html ページが表示されてしまったりする。
なお、今回は location
と proxy_path
、そして SSL 証明書関連の設定はクリアできていることを前提としている。そこまでの設定は、ネットの情報も多いのでどうにかクリアできるはず。
問題原因
問題の主な原因は次の2つ。
- レスポンスの HTML に絶対パスのURLが含まれていること
- リダイレクトの
Location
がずれてしまうこと
リクエスト元のクライアントからはコンテキストパス付きURLでアクセスするが、リバースプロキシ先のWebサービスからのレスポンスでは絶対パスのURLにコンテキストパスが付かない。そのため、クライアントは最初のレスポンス(index.htmlなど)は取得できても、レスポンスに記述されている css ファイルなどを追加取得しようとすると 404 エラーとなってしまう。
だからと言って、HTML ファイルに記述するURLをすべて相対パスにするのは無理だ。css ファイルは href="/css/styles.css"
のように絶対パスで記述してスタイルを統一するのが定石だろう。
また、Webアプリの場合、ログイン時にリダイレクトが使用されたりするが、リダイレクトを指示するレスポンスでは、Location
や Refresh
に絶対パスが記述されていることが多い。
この Location
と Refresh
の書き換えには、nginx の proxy_redirect
ディレクティブで対処できるのだが、絶対パスの値がWebアプリによって異なるためその値に応じて合わせこみが必要になる。
対策
リバースプロキシ先のWebアプリがコンテキストパス対応の場合
-
リバースプロキシ先のWebアプリがコンテキストパス対応の場合、Webアプリをコンテキストパス付きでセットアップし、リバースプロキシ側でも同じコンテキストパスを利用する。
コンテキストパスが同じであれば、
proxy_path
ディレクティブとproxy_redirect default;
の一行で概ね課題は解消できる。
location /some-service/ {
proxy_pass http://192.168.1.10/some-service/;
proxy_redirect default;
...
}
- あとは、
proxy_set_header
ディレクティブでプロキシ転送時に必要なヘッダー情報を加えておく。
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
- なお、コンテキストパス対応のWebアプリであっても、運用途中でコンテキストパス付きに切り替えることは困難なため、その場合はコンテキストパス非対応の場合と同様に対応する。
コンテキストパス非対応の場合
-
proxy_redirect
ディレクティブではレスポンスヘッダーのLocation
とRefresh
しか書き換えられない。レスポンス中の絶対パスはsub_filter
ディレクティブを利用して書き換える。 - 書き換え箇所は
http(s)://host-name/
と/
から始まる両方の絶対パス。 -
http(s)://host-name/
から始まる絶対パスはhttps://reverse-proxy.your-domain.com/context-path/
に書き換える。
sub_filter 'http://192.168.1.10/' '$scheme://$host/some-service/';
sub_filter 'http:\/\/192.168.1.10\/' '$scheme:\/\/$host\/some-service\/';
-
/
から始まる絶対パスはhref
,src
,action
,url
の箇所を対象に書き換える
sub_filter 'href="/' 'href="/some-service/';
sub_filter 'src="/' 'src="/some-service/';
sub_filter 'action="/' 'action="/some-service/';
sub_filter 'url("/' 'url("/some-service/';
- Javascript 中では
\/\/hostname\/
のようにエスケープ表記されているパスも書き換える。
sub_filter 'http:\/\/192.168.1.10\/' '$scheme:\/\/$host\/some-service\/';
-
絶対パスの
http(s)://host-name/
の部分はWebアプリがプロキシ転送ヘッダ情報(X-Forwarded-Host
,X-Forwarded-Port
,X-Forwarded-Proto
)を解釈するか否かによって変わってくる。そのため、proxy_set_header
ディレクティブでプロキシ転送ヘッダ情報を設定したうえで、レスポンスの中身を確認しながら絶対パスを書き換える。書き換え対象の絶対パスを把握するには、一時的に
proxy_redirect off;
を記述して書き換え前の値を現物確認するとよい。
# proxy_redirect off; # 書き換え対象のレスポンス確認用
# proxy_redirect $scheme://$host/ /some-service/; # Webアプリのレスポンスに応じて追加
proxy_redirect / /some-service/;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
-
sub_filter
ディレクティブによる書き換えが複数箇所に適用されるように次の設定を加える。
sub_filter_once off;
- レスポンスが gzip 圧縮されていると
sub_filter
ディレクティブで書き換えできないため、リクエストを転送時にproxy_set_header Accept-Encoding "";
を加えてレスポンスを非圧縮で取得する。
proxy_set_header Accept-Encoding "";
- リバースプロキシからクライアントに転送するレスポンスを gzip 圧縮するためには、
gzip on;
ほかの設定を加える。
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/css text/javascript application/javascript application/xml application/json;
設定例
location /some-service/ {
set $context "/some-service/";
# See http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
proxy_pass http://192.168.1.10/;
# See http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect
# proxy_redirect $scheme://$host/ $context; # Webアプリのレスポンスに応じて追加
proxy_redirect / $context;
proxy_redirect default;
# proxy_redirect http://192.168.1.10/ $context; # default と同義
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# See https://qiita.com/no1zy_sec/items/dfccd91cf76bf73b7754
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Accept-Encoding ""; # sub_filter のためにレスポンス非圧縮
# See http://nginx.org/en/docs/http/ngx_http_sub_module.html
sub_filter 'href="/' 'href="$context';
sub_filter 'src="/' 'src="$context';
sub_filter 'url("/' 'url("$context';
sub_filter 'action="/' 'action="$context';
sub_filter 'href=\'/' 'href=\'$context';
sub_filter 'src=\'/' 'src=\'$context';
sub_filter 'url(\'/' 'url(\'$context';
sub_filter 'action=\'/' 'action=\'$context';
sub_filter 'http://$proxy_host/' '$scheme://$host$context';
sub_filter 'http:\/\/$proxy_host\/' '$scheme:\/\/$host\/some-service\/';
# sub_filter '//$host/' '//$host$context'; # Webアプリのレスポンスに応じて選択
# sub_filter '\/\/$host\/' '\/\/$host\/some-service\/';
sub_filter_once off;
# See https://qiita.com/cubicdaiya/items/2763ba2240476ab1d9dd
# See also http://nginx.org/en/docs/http/ngx_http_gzip_module.html
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/css text/javascript application/javascript application/xml application/json;
}
参考
- http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect
- http://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter
- https://stackoverflow.com/questions/31893211/http-sub-module-sub-filter-of-nginx-and-reverse-proxy-not-working
- https://qiita.com/no1zy_sec/items/dfccd91cf76bf73b7754
- https://qiita.com/cubicdaiya/items/2763ba2240476ab1d9dd
■