はじめに
なんとなくわかりにくいリバースプロキシの理解のため、ローカルでリバースプロキシを立ててみる記事です。
この記事の範囲:Dockerでnginxのリバースプロキシを立て、ブラウザからWebサーバにリバースプロキシを介してリクエストを送りレスポンスを確認するところまで
プロキシとリバースプロキシの違いまとめも書いているのでご覧ください。
実際に(ローカルで)リバースプロキシを立ててみよう
リバースプロキシは広い概念で、HTTPリバースプロキシやTCPリバースプロキシなどがあります。
ここでは、HTTPリバースプロキシの一つであるnginxをローカルで立ててみましょう。
nginxって何
nginxとは、Webサーバからリバースプロキシまでなんでもこなせる優れたサーバサイドソフトウェアです。
一般に、Webサーバは専用のアプリケーション(Apacheやnginx)を使い、特定のポートでListenして、クライアントとTCPコネクションを結びます。クライアントからHTTPリクエストが送られてきたら、なんらかの処理をした後、送信元にレスポンス(例えばindex.htmlなど)を返します。
nginxは、Webサーバのアプリケーションでもあるので、今回はこの用途でも使ってみましょう。
今回の構成は、ある一つのWebサイトの中に犬用コンテンツと猫用コンテンツの二つがあり、URLのパスでそれぞれにアクセスできるものを考えます。
HTTPリバースプロキシでは、クライアントから受け取ったリクエストのHTTPのパスで異なるサーバに割り振ることができます。
クライアントからはHTTPリバースプロキシサーバとしてのnginxに /dog
もしくは /cat
のパスで localhost:80
(もしくは単に localhost
)にアクセスします。
すると、リバースプロキシではパスを解析して、パスごとにあらかじめ設定したWebサーバにルーティングを行います。
リバースプロキシは、Webサーバからレスポンスが返ってくると、そのレスポンスをまるで自分が処理したかのようにクライアントに返します。
目標
-
localhost/cat
にリクエストを送ると、猫好きのためのページを表示 -
localhost/dog
にリクエストを送ると、犬好きのためのページを表示 - 猫用、犬用サーバは別に分ける
- クライアントは、それらのサーバの存在やポート番号を知らないでよい
ディレクトリ構成
.
├── docker-compose.yml
├── cat-server
│ └── index.html
├── dog-server
│ └── index.html
└── reverse-proxy
└── nginx.conf
ソースコードはGitHub(zawawahoge/reverse-proxy)にアップロードしています。cloneして docker-compose up
したらローカルで実際にこの構成を立てることができます。
実装詳細
docker-compose
version: '3'
services:
dog-server:
image: nginx
container_name: 'dog-container'
volumes:
- ./dog-server:/usr/share/nginx/html
ports:
- 7000:80
cat-server:
image: nginx
container_name: 'cat-container'
volumes:
- ./cat-server:/usr/share/nginx/html
ports:
- 7001:80
reverse-proxy:
image: nginx
volumes:
- ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80
これを含むディレクトリで docker-compose up
を実行することで、二つのWebサーバと一つのリバースプロキシが起動します。
各コンテナに対し、必要なファイルをvolumeとしてマウントしています。これらのファイルの中身についてもみてみます。
インデックスページ(猫用、犬用)
犬用と猫用サーバのそれぞれについて、インデックスページを作っておきましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>犬好きのためのページ</title>
</head>
<body>
<h1>犬好きのためのページ</h1>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>猫好きのためのページ</title>
</head>
<body>
<h1>猫好きのためのページ</h1>
</body>
</html>
非常にシンプルなものですが、これで十分でしょう。
リバースプロキシ用の設定ファイル
続いて、nginxリバースプロキシ用の設定ファイルを書く必要があります。
events {
worker_connections 16;
}
http {
server {
listen 80;
server_name localhost;
location /dog {
proxy_pass http://host.docker.internal:7000/;
proxy_redirect off;
}
location /cat {
proxy_pass http://host.docker.internal:7001/;
proxy_redirect off;
}
}
}
nginxの設定ファイルの書き方
events
nginxの設定ファイルは、**コンテクスト(context)**と呼ばれる {}
で囲まれたブロックで構成されています。
最初のコンテクストである events
については公式(英語)で説明されています。
Syntax: events { ... }
Default: —
Context: main
Provides the configuration file context in which the directives that affect connection processing are specified.
コネクション全体に関する設定を意味するコンテクストです。
worker_connections
は、同時に接続できるワーカープロセスの数を表します(デフォルト1)。
http
http
コンテクストには、HTTPサーバについての設定を書きます。
Provides the configuration file context in which the HTTP server directives are specified.
server
server
コンテクストは、仮想サーバのための設定をします。
公式ドキュメントに詳しいドキュメントが書かれています。
Syntax: server { ... }
Default: —
Context: http
Sets configuration for a virtual server. (略)
下のサイトが参考になります。
nginx連載4回目: nginxの設定、その2 - バーチャルサーバの設定
HTTPリクエストを受け取ってからの流れ
HTTPリクエストが来ると、 http
コンテクストにある server
コンテクストで当てはまるものを探していきます。 server
コンテクストには、 listen
と server_name
の二つが記述され、それらが条件となります。
- まずは、
listen
で指定されたIPアドレス(またはホスト)とポート番号がリクエストのものと一致するかどうか調べます。 - 当てはまったら、
server_name
がHTTPヘッダのHost
と同じであれば、そのserver
コンテクストに書かれた処理をすることになります。
今回利用するのは次のような server
コンテキストです。
server {
listen 80;
server_name localhost;
# 処理...
}
意味としては、 listen
でポート80にきたリクエストかどうか判定し、 server_name
で リクエストヘッダの Host
が localhost
と一致しているかどうかを判定します。どちらも当てはまるようなら続く処理が実行されるというものです。
今回の場合、リバースプロキシとして機能させるため、HTTPのURL内のパスを解析し、別のWebサーバにルーティングする処理をすることになります。
location /dog {
proxy_pass http://host.docker.internal:7000/;
proxy_redirect off;
}
location /cat {
proxy_pass http://host.docker.internal:7001/;
proxy_redirect off;
}
続いて、パスが当てはまる location
コンテクストが探され、 /dog
というパスであれば、 proxy_pass
に書かれたURIにルーティングされます。
パスがさらに長い場合、例えば localhost/dog/list
であれば、 http://host.docker.internal:7000/list
にルーティングされることになります。
proxy_passの注意点
ややこしいことですが、 proxy_pass
はURIの場合とそうでない場合でルーティングのパスが変わるので注意してください。
nginxのproxy_passの注意点
Dockerコンテナからみたホストのポート
http://host.docker.internal
はDockerのコンテナから見たホストを指していて、コンテナ内からホストのポート 7000
にアクセスする場合にはこのように書くと名前解決してくれてホストそのもののポートにアクセスできます。
このような書き方が必要になるのは、Dockerコンテナ内で localhost
と記述した時は、Dockerを起動しているホストではなくコンテナ自身を指す仕様になっているからです。もちろん、Dockerを使わずにnginxを立ち上げた場合は localhost
に置き換えて構いません。
要するに、 localhost/dog
に来た場合は、Dockerを起動しているホストのポート 7000
にHTTPリクエストを送信する仕組みになっています( /cat
の場合も同様に 7001
)。
結果
docker-compose up
を実行し、curlコマンドに localhost/dog
でリクエストを送り、レスポンスを見てみましょう。
curl --verbose localhost/dog
> GET /dog HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
< HTTP/1.1 200 OK
< Server: nginx/1.17.0
< Date: Mon, 01 Jul 2019 13:38:14 GMT
< Content-Type: text/html
< Content-Length: 264
< Connection: keep-alive
< Last-Modified: Sun, 30 Jun 2019 13:43:59 GMT
< ETag: "5d18bc9f-108"
< Accept-Ranges: bytes
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>犬好きのためのページ</title>
</head>
<body>
<h1>犬好きのためのページ</h1>
</body>
* Connection #0 to host localhost left intact
</html>%
各コンテナ(リバースプロキシ・Webサーバ)のログをみてみましょう。
reverse-proxy_1 | 172.31.0.1 - - [01/Jul/2019:13:38:14 +0000] "GET /dog HTTP/1.1" 200 264 "-" "curl/7.54.0"
dog-container | 172.31.0.1 - - [01/Jul/2019:13:38:14 +0000] "GET / HTTP/1.0" 200 264 "-" "curl/7.54.0" "-"
結局何が起こったの?
注目すべきなのは、クライアント側(curlを叩いた側)は、 dog-container
というWebサーバの存在を全く知らないでも通信できているという点です。
つまり、クライアントからは、 localhost/dog
というGETリクエストを localhost:80
に送り、そこからレスポンスを受け取ったように見えますが、実際は dog-container
という(本当の)Webサーバからレスポンスが返ってきているのです。
もちろん猫用もみれます。
curl localhost/cat
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>猫好きのためのページ</title>
</head>
<body>
<h1>猫好きのためのページ</h1>
</body>
</html>%
もちろんブラウザからもみれます。
バッチリですね。
まとめ
リバースプロキシの理解のために、実際にリバースプロキシを介したリクエストとレスポンスを確認しました。
リバースプロキシを使うことで、Webサーバを外から直接アクセスできないようにするだけでなく、リクエストに応じてサーバを自在に変えることができるため、柔軟なサーバ構成を考えられるようになるかと思います。
今回の実装のソースコードはGitHub(zawakin/reverse-proxy)にアップロードしていますので、ぜひローカルで試してみてください。