nginx.confを1行ずつ解剖
nginxの設定が読めれば障害対応もリバースプロキシ構築も怖くない。実戦形式のnginx.confを1行ずつ分解し、locationの優先順位やproxy_passの罠まで現場で効く読み方を身につける。
- nginxはマスターと複数ワーカーで動く。worker_processes autoはCPUコア数へ合わせる指定で、worker_connectionsは1ワーカーの同時接続上限を定め、プロキシ接続も数える。
- locationの評価は完全一致の=が最優先。次に最長の前方一致を確定し、それが^~なら即採用、そうでなければ~と~*の正規表現を記述順に試し、どれにも合わなければ前方一致の結果に戻る。記述順が効くのは正規表現同士だけだ。
- proxy_passは末尾スラッシュで挙動が変わり、URI付きならlocation一致部を置換する。既定のHostは$proxy_hostなので、proxy_set_headerでHostやX-Forwarded-Forを指定する。
この設定ファイルは何者か
nginx.confは、Webサーバー兼リバースプロキシであるnginxの挙動をすべて決める中枢の設定ファイルだ。多くのLinux環境では /etc/nginx/nginx.conf が読解の起点となり、静的配信、TLS終端、ロードバランシングといったnginxの仕事は、最終的にすべてこのファイルの記述へ行き着く。
文法は驚くほど小さい。セミコロンで終わる単純ディレクティブと、波括弧で子を束ねるブロックディレクティブの2種類だけで、ブロックはコンテキストと呼ばれる。外側のコンテキストに書いた設定は原則として内側へ継承され、内側で同名のディレクティブを書けば上書きできる。この「外で共通、内で例外」という継承構造こそ、nginx.confを読むときの最大の手掛かりだ。
配布パッケージのnginx.confは本体が短く、実体はincludeで読み込まれる別ファイル側にあることが多い。読解はinclude構造の把握から始まる。
なお、そもそもなぜリバースプロキシを挟むのかという設計判断はシステム設計に譲り、本稿はファイルの読解に集中する。
横にスクロール
1行ずつ解剖
題材は、静的ファイル配信とAPIサーバーへの転送を1台で担う実務最小限の構成だ(TLSは省略)。
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
gzip on;
gzip_types text/css application/javascript application/json;
upstream app_backend {
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
server {
listen 80 default_server;
server_name example.com;
root /var/www/public;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://app_backend/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
worker_processes と events — プロセスモデルの土台
nginxは、設定の読み込みとワーカー管理を担うマスタープロセスと、実際の通信をさばく複数のワーカープロセスで構成される。worker_processes auto; はワーカー数をCPUコア数に自動で合わせる指定で、既定値の1のまま運用する理由はほぼない。
eventsブロックは接続処理の設定を置く場所で、中身が空でも省略できない。worker_connections は1ワーカーが同時に保持できる接続数の上限(既定値512)だが、クライアントとの接続だけでなくバックエンドへのプロキシ接続もこの数を消費する。つまりリバースプロキシでは1リクエストが2接続を使う計算になる。
http・server・location — 3層の入れ子構造
httpブロックの下に仮想サーバーを表すserverブロックが並び、その中でURIのパターンごとにlocationブロックが分かれる。gzip on; のようにhttp直下へ書いた設定は、その下のすべてのserverとlocationに継承される。逆に言えば、あるlocationの挙動を知りたければ、locationからserver、httpへと外側へ遡って読めばよい。
include mime.types; は拡張子とContent-Typeの対応表の読み込みで、これを欠くとCSSやJavaScriptが正しい型で配信されない。gzipは既定でoffであり、しかもonにしただけでは圧縮対象がtext/htmlに限られる。gzip_types でCSS・JavaScript・JSONを追加して初めて実用になる(text/htmlは常に対象)。圧縮やキャッシュを含むHTTP配信の全体像はWebで扱っている。
server_name とデフォルトサーバー
serverブロックの選択は、リクエストのHostヘッダとserver_nameの突き合わせで決まる。どのserver_nameにも一致しないときは、そのlisten対象(アドレスとポートの組)のデフォルトサーバーが処理する。既定では設定ファイル上で最初に現れたserverブロックがその役だが、listen 80 default_server; のように明示するのが安全だ。未知のHostを意図しない仮想サーバーが拾う事故は、この1語で防げる。
location の一致優先順位
locationには修飾子があり、評価は書いた順ではなく次の優先順位に従う。
| 優先順 | 修飾子 | 挙動 |
|---|---|---|
| 1 | = | 完全一致。一致したら即決定 |
| 2 | ^~ | 最長の前方一致がこれなら正規表現を見ずに決定 |
| 3 | ~ / ~* | 正規表現(大文字小文字を区別する/しない)。記述順に評価し最初の一致で決定 |
| 4 | 修飾子なし | 前方一致のうち最長のもの |
正確なアルゴリズムはこうだ。まず=を探し、あれば即決定。次に前方一致をすべて調べて最長を記憶する。それが^付きなら正規表現を飛ばして採用。そうでなければと~*を記述順に試し、最初の一致が勝つ。どれにも一致しなければ、記憶した最長の前方一致へ戻る。
location /api/ { # 前方一致
proxy_pass http://app_backend/;
}
location ~* \.(png|jpg)$ { # 正規表現
expires 7d;
}
この例で /api/logo.png への要求は、前方一致の /api/ ではなく正規表現側に入る。前方一致がどれだけ長くても、^~で保護しない限り正規表現が優先されるからだ。
upstream とロードバランシング
upstreamブロックは転送先サーバーのグループに名前を付け、proxy_passから参照させる。既定の振り分けはラウンドロビンで、serverにweightパラメータを付ければ重み付けもできる。least_conn;(アクティブ接続が最少の先へ)や ip_hash;(同一クライアントを同じ先へ固定)へは1行で切り替えられる。さらに、応答に失敗したサーバーをmax_failsとfail_timeoutの条件で一時的に切り離す受動的なヘルスチェックが既定で働く。
proxy_pass — URIの有無で挙動が変わる
proxy_passは要求を別サーバーへ転送する中心ディレクティブだが、転送先にURI部を書くかどうかで挙動が一変する。location /api/ に /api/users が来た場合で比べる。
| 書き方 | バックエンドが受け取るパス |
|---|---|
| proxy_pass http://app_backend;(URIなし) | /api/users(そのまま渡る) |
| proxy_pass http://app_backend/;(末尾スラッシュ=URIあり) | /users(location一致部分が置換される) |
URIを書かなければ要求URIは無加工で渡る。スラッシュ1文字でもURIを書くと、locationに一致した部分がそのURIへ置き換えられる。サンプルは意図的に末尾スラッシュ付きとし、バックエンドには /api/ を取り除いたパスが届く。どちらを意図しているのかを常に自覚したい。
proxy_set_header — Host と X-Forwarded-For
プロキシを挟むと、バックエンドから見える通信相手はnginxだけになる。既定で書き換えられるヘッダは2つあり、Hostは $proxy_host(proxy_passに書いたホスト名)に、Connectionはcloseになる。クライアントのIPアドレスは何もしなければ伝わらない。そこで proxy_set_header Host $host; で元のホスト名を復元し、X-Forwarded-Forに $proxy_add_x_forwarded_for を設定して経由アドレス列の末尾にクライアントIPを追記する。アプリのリダイレクト先やアクセス元判定が狂ったら、まずこの2行を疑う。
try_files — 静的配信の定番イディオム
try_files $uri $uri/ /index.html; は、指定パスのファイル、次にディレクトリの順で存在を確かめ、どれも無ければ最後の引数へ内部リダイレクトする。SPAで任意のパスをindex.htmlへ集約する定番であり、後述する「if is evil」を回避する最有力の道具でもある。
つまずきやすい点
第一に「if is evil」。nginxのifはrewriteモジュールの一部で、location内では暗黙の入れ子locationを作る独特の動きをする。安全に使えるのは実質returnとrewriteだけで、他のディレクティブと組むと設定が無視されたり予期しない応答になったりする。ファイル存在の分岐はtry_files、値の変換はmapで置き換えるのが定石だ。
第二に、proxy_passの末尾スラッシュ。前述の置換規則を知らずにスラッシュを1文字消すだけで、バックエンドへ届くパスが一斉に変わり404が噴き出す。差分レビューで最も見落とされやすい1文字と言っていい。
第三に、locationの優先順位の思い込み。「上に書いた方が勝つ」「長い方が常に勝つ」はどちらも誤りだ。前方一致同士は記述順と無関係に最長が勝ち、正規表現は^~で保護しない限り前方一致より優先される。記述順が意味を持つのは正規表現同士だけである。
nginx -t は構文と参照ファイルを検証し、nginx -s reload は新しいワーカーを起動してから古いワーカーを緩やかに終了させるため、接続を切らずに設定を反映できる。reloadは設定が壊れていれば旧設定のまま動き続けるが、そのエラーを見逃したまま再起動すると今度は起動自体に失敗してサービスが止まる。テストと反映は常にセットで行う。
まとめ
nginx.confは、マスターとワーカーというプロセスモデル、httpからserver、locationへ継承される3層構造、locationの優先順位、そしてproxy_passの置換規則という少数の原理で読み切れる。1行ずつ意味を確かめ、nginx -t を通してから nginx -s reload で反映する。この往復さえ守れば、設定ファイルは怖い相手ではない。コンテナ時代のもう1つの必修ファイルはDockerfileを1行ずつ解剖で精読する。
設定ファイル解剖の記事ガイド
nginx.confを1行ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
設定ファイル
比較で見る軸
難易度: advanced / カテゴリ: 設定ファイル解剖 / タグ数: 4
導入後に効く点
locationの評価は完全一致の=が最優先。次に最長の前方一致を確定し、それが^~なら即採用、そうでなければ~と~*の正規表現を記述順に試し、どれにも合わなければ前方一致の結果に戻る。記述順が効くのは正規表現同士だけだ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 設定ファイル解剖
- タグ数
- 4
判断チェックリスト
- 自社の用途が「設定ファイル / nginx」に近いか確認する。
- 強みである「nginxはマスターと複数ワーカーで動く。worker_processes autoはCPUコア数へ合わせる指定で、worker_connectionsは1ワーカーの同時接続上限を定め、プロキシ接続も数える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。