概要
Webアプリケーション開発でNginxをWebサーバとして使用しているプロジェクトが多いので今回は
- ローカル環境
- 本番環境
別で
- NginxのDockerfileおよびnginx.confファイルの作成方法
- フロントエンドとバックエンドの連携方法
等について解説していきたいと思います
前提
- 今回は例としてフレームワークはDjango、WSGIサーバはGunicornを使用します
- インフラはAWSを使用する前提でお話しします
プロキシサーバとリバースプロキシサーバの違い
プロキシサーバはインターネット(クライアント)と通信する際にクライアントが直接Webサーバに接続せずに別のサーバーに経由する役割を担うサーバーのことです
プロキシを経由することで直接アクセスできないWebサーバにアクセスすることができるなどの利点があります
一方で、リバースプロキシサーバはクライアントからのリクエストをバックエンドサーバーに中継する役割を担うサーバーです
まとめるとプロキシサーバは代理(Proxy)するのはインターネット(クライアント)に対してリバースプロキシはその逆でパックエンドサーバの代理になります
今回説明するNginxはクライアントからの通信をプロキシし、バックエンドのAPIサーバに送信するプロキシサーバの役割を担います
ローカル環境
ローカル環境ではDockerfileと一緒にdocker-compose.ymlも使用するので
- docker-compose.yml
- Dockerfile
- nginx.conf
の順に解説していきます
docker-compose.yml
以下が
- Django
- MySQL
- Nginx
- Node.js
用のdocker-compose.ymlです
ローカル環境ではNginxを使ってフロントエンドとバックエンドを連携させる際にDockerのnetworkを使います
今回はtestnetというネットワークを作成します
version: '3.9'
services:
app:
container_name: app
build:
context: .
dockerfile: containers/django/Dockerfile
volumes:
- ./application:/code
- ./static:/static
ports:
- '8000:8000'
# デバッグ用ポート
- '8080:8080'
command: sh -c "/usr/local/bin/entrypoint.sh"
stdin_open: true
tty: true
env_file:
- .env
nginx:
container_name: web
build:
context: .
dockerfile: containers/nginx/Dockerfile
volumes:
- ./static:/static
ports:
- 80:80
depends_on:
- app
front:
container_name: front
build:
context: .
dockerfile: containers/front/Dockerfile
volumes:
- ./frontend:/code
- node_modules_volume:/frontend/node_modules
command: sh -c "npm run dev"
ports:
- '3000:3000'
environment:
- CHOKIDAR_USEPOLLING=true
- RESTAPI_URL=http://localhost/back
volumes:
static:
networks:
default:
name: testnet
Dockerfile
後述するnginx.confをコピーするよう設定します
FROM nginx:1.21
RUN rm -f /etc/nginx/conf.d/*
COPY ./containers/nginx/nginx.conf /etc/nginx/conf.d/
nginx.conf
ローカル環境で使うNginxの設定ファイルです
upstream front {
server host.docker.internal:3000;
}
upstream back {
server host.docker.internal:8000;
}
server {
listen 80;
server_name localhost;
client_max_body_size 20M;
location / {
proxy_pass http://front/;
}
location /back/ {
# X-Forwarded-Hostヘッダにバックエンドのホスト名とポートを指定
proxy_set_header X-Forwarded-Host $host:$server_port;
# X-Forwarded-Serverヘッダにバックエンドのホスト名を指定
proxy_set_header X-Forwarded-Server $host;
# X-Forwarded-Forヘッダにリクエストを送ったクライアントまたはプロキシのIPアドレスの履歴(リスト)を設定
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 上記の情報のヘッダが今回だとMacの8000番ポートに最終的に転送される
proxy_pass http://back/;
}
location /_next/webpack-hmr {
proxy_pass http://front/_next/webpack-hmr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
一つずつ解説します
upstream
upstreamディレクティブを使用して
- バックエンドサーバ
- フロントエンドサーバ
を定義しています
その際はserverの後にserver名を記載します
今回はDockerコンテナとDockerホスト(Mac)間で通信させます
host.docker.internal
はDockerの機能の1つでこれを使用するとIPアドレスを指定せずにコンテナからホスト(Mac)に簡単にアクセスできます
docker-compose.ymlのポートでフロントエンドは3000、バックエンドは8000にしてるのでこちらも同様にポート番号をそろえます
upstream front {
server host.docker.internal:3000;
}
upstream back {
server host.docker.internal:8000;
}
server
80番ポートを経由してプロキシする設定です
サーバ名はローカル環境を使っているのでlocalhostです
server {
listen 80;
server_name localhost;
Nginxからフロントエンドにプロキシする設定を記載します
upstreamでfrontと記載しているので名前を合わせます
location / {
proxy_pass http://front/;
}
Nginxからバックエンドにプロキシする設定を記載します
upstreamでbackと記載しているので名前を合わせます
location /back/ {
# X-Forwarded-Hostヘッダにバックエンドのホスト名とポートを指定
proxy_set_header X-Forwarded-Host $host:$server_port;
# X-Forwarded-Serverヘッダにバックエンドのホスト名を指定
proxy_set_header X-Forwarded-Server $host;
# X-Forwarded-Forヘッダにリクエストを送ったクライアントまたはプロキシのIPアドレスの履歴(リスト)を設定
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 上記の情報のヘッダが今回だとMacの8000番ポートに最終的に転送される
proxy_pass http://back/;
}
Webpackerの設定
NginxからNext.jsのWebpackerへリバースプロキシする設定です
公式ドキュメントに記載の通り、Nginxのversion1.3.13以降ではホットモジュールリローディング(HMR)を有効化するには以下のヘッダーを明示的に渡す必要があります
location /_next/webpack-hmr {
proxy_pass http://front/_next/webpack-hmr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
To turn a connection between a client and server from HTTP/1.1 into WebSocket, the protocol switch mechanism available in HTTP/1.1 is used.
There is one subtlety however: since the “Upgrade” is a hop-by-hop header, it is not passed from a client to proxied server. With forward proxying, clients may use the CONNECT method to circumvent this issue. This does not work with reverse proxying however, since clients are not aware of any proxy servers, and special processing on a proxy server is required.
Since version 1.3.13, nginx implements special mode of operation that allows setting up a tunnel between a client and proxied server if the proxied server returned a response with the code 101 (Switching Protocols), and the client asked for a protocol switch via the “Upgrade” header in a request.
As noted above, hop-by-hop headers including “Upgrade” and “Connection” are not passed from a client to proxied server, therefore in order for the proxied server to know about the client’s intention to switch a protocol to WebSocket, these headers have to be passed explicitly
本番環境
本番環境ではdocker-compose.ymlを使わずにECS Fargateを使用するので
- NginxのDockerfile
- nginx.conf
の順に解説していきます
Dockerfile
FROM --platform=linux/x86_64 nginx:stable-alpine
RUN rm -f /etc/nginx/conf.d/*
COPY ./containers/nginx/nginx.conf /etc/nginx/conf.d/
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
Apple Silicon Macを使用する際はNginxのDockerfileをAWS上で使用する際に
--platform=linux/x86_64
を指定する必要があります
M1Macがarmのアーキテクチャになっているのに対し、ECS上ではlinux/x86_64になっており、
ECSをbuildする際にplatform errorが発生するからです
そこで上記のコマンドを追記してDockerfile内のplatformをECSと同じになるよう設定します
また、Nginxはデフォルトでバックグラウンド(デーモンがオンの状態)で実行されてしまいます
Cloud Watch上でログが見れるようフォアグラウンドで実行できるようデーモンをオフにします
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
nginx.conf
Nginxの本番環境の設定ファイルです
upstream gunicorn {
# gunicornとdjangoがunixソケットを通じて通信する
server unix:///code/tmp/gunicorn_socket;
}
server {
listen 80;
server_name example.com api.example.com;
# Nginxのバージョン情報を非表示にする
# サーバ情報を隠すことでセキュリティ上のリスクを軽減させる
server_tokens off;
# ファイルサイズの変更、デフォルト値は1M
client_max_body_size 5M;
# HTTP レスポンスヘッダの Content_Type に付与する文字コード
charset utf-8;
# ログ設定
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# API 通信
# プロキシ先のGunicornに対して、元のクライアントIPアドレスやヘッダ情報を転送
location /api {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_read_timeout 3600;
proxy_pass http://gunicorn;
}
# ヘルスチェック用のレスポンスを転送
location /api/health {
empty_gif;
access_log off;
break;
}
# HTTP 通信をタイムアウトせずに待つ秒数
keepalive_timeout 60;
}
1つずつ解説します
upstream
upstream gunicorn {
# gunicornとdjangoがunixソケットを通じて通信する
server unix:///code/tmp/gunicorn_socket;
}
GunicornとDjangoをソケット通信させる設定を行います
ソケット通信することでインターネットを経由せずに直接データ間のやり取りができるのでセキュリティもパフォーマンスも向上します
APIとの通信の設定
ローカルの時とほぼ同じ設定になります
proxy_passだけupstreamで定義したgunicornを設定します
# API 通信
# プロキシ先のGunicornに対して、元のクライアントIPアドレスやヘッダ情報を転送
location /api {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_read_timeout 3600;
proxy_pass http://gunicorn;
}
ヘルスチェック
ヘルスチェックの設定を記載します
ヘルスチェックに関してはaccess_logの設定をoffにします
# ヘルスチェック用のレスポンスを転送
location /api/health {
empty_gif;
access_log off;
break;
}
まとめ
Nginxの設定ファイルやDockerfileをなんとなくでしか書いていなかったんでまとめてみました
ローカル上でもAWS上でも使うのに必須の知識なので覚えておいて損はないかと思います
参考