Help us understand the problem. What is going on with this article?

実践編ーDockerを使ってnginxでリバースプロキシを立ててみる

More than 1 year has passed since last update.

はじめに

なんとなくわかりにくいリバースプロキシの理解のため、ローカルでリバースプロキシを立ててみる記事です。

この記事の範囲:Dockerでnginxのリバースプロキシを立て、ブラウザからWebサーバにリバースプロキシを介してリクエストを送りレスポンスを確認するところまで

プロキシとリバースプロキシの違いまとめも書いているのでご覧ください。

実際に(ローカルで)リバースプロキシを立ててみよう

リバースプロキシは広い概念で、HTTPリバースプロキシやTCPリバースプロキシなどがあります。

ここでは、HTTPリバースプロキシの一つであるnginxをローカルで立ててみましょう。

nginxって何

nginxとは、Webサーバからリバースプロキシまでなんでもこなせる優れたサーバサイドソフトウェアです。

一般に、Webサーバは専用のアプリケーション(Apacheやnginx)を使い、特定のポートでListenして、クライアントとTCPコネクションを結びます。クライアントからHTTPリクエストが送られてきたら、なんらかの処理をした後、送信元にレスポンス(例えばindex.htmlなど)を返します。

nginxは、Webサーバのアプリケーションでもあるので、今回はこの用途でも使ってみましょう。

今回の構成は、ある一つのWebサイトの中に犬用コンテンツと猫用コンテンツの二つがあり、URLのパスでそれぞれにアクセスできるものを考えます。
image.png

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

docker-compose.yml
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としてマウントしています。これらのファイルの中身についてもみてみます。

インデックスページ(猫用、犬用)

犬用と猫用サーバのそれぞれについて、インデックスページを作っておきましょう。

dog-server/index.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>
cat-server/index.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リバースプロキシ用の設定ファイルを書く必要があります。

reverse-proxy/nginx.conf
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 コンテクストには、 listenserver_name の二つが記述され、それらが条件となります。

  1. まずは、 listen で指定されたIPアドレス(またはホスト)とポート番号がリクエストのものと一致するかどうか調べます。
  2. 当てはまったら、 server_name がHTTPヘッダの Host と同じであれば、その server コンテクストに書かれた処理をすることになります。

今回利用するのは次のような server コンテキストです。

    server {
        listen 80;
        server_name localhost;
        # 処理...
    }

意味としては、 listen でポート80にきたリクエストかどうか判定し、 server_name で リクエストヘッダの Hostlocalhost と一致しているかどうかを判定します。どちらも当てはまるようなら続く処理が実行されるというものです。

今回の場合、リバースプロキシとして機能させるため、HTTPのURL内のパスを解析し、別のWebサーバにルーティングする処理をすることになります。

serverコンテクストの処理
        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
HTTPリクエストヘッダ
> GET /dog HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
HTTPレスポンスヘッダ
< 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
HTTPレスポンスbody
<!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サーバ)のログをみてみましょう。

nginxのログ
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>%

もちろんブラウザからもみれます。

image.png

バッチリですね。

まとめ

リバースプロキシの理解のために、実際にリバースプロキシを介したリクエストとレスポンスを確認しました。

リバースプロキシを使うことで、Webサーバを外から直接アクセスできないようにするだけでなく、リクエストに応じてサーバを自在に変えることができるため、柔軟なサーバ構成を考えられるようになるかと思います。

今回の実装のソースコードはGitHub(zawawahoge/reverse-proxy)にアップロードしていますので、ぜひローカルで試してみてください。

参考サイト

zawawahoge
ナレッジワークという創業まもないスタートアップにBackend engineer(Go)として参加中 Python布教活動に励んでいたが、いつの間にかGoの魔の吸引力にやられてしまい、Goばかり書いている 元地球物理研究者見習い(修士) 十年後読んでも褪せない記事が書きたい。
https://kwork.studio/
knowledgework
ナレッジワークを開発しています
https://kwork.studio
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away