nginxのメンテナンスモードはmapで組む

  • nginx
  • HTTP

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

メンテナンス作業中だけ全体を503にして、専用ページを出すやつを組んだ。よくある要件だけど、条件分岐をifで書くと後で除外パターンを増やすたびにネストが深くなるので、最初からmapディレクティブでテーブル化しておいた。

やりたいこと

  • /var/tmp/do_maintenance が存在する間は全体を503にする
  • /maintenance/ 配下とヘルスチェック用URLは常に除外
  • 動作確認用の許可IP(自分の環境やオフィスのグローバルIP)は除外
  • 許可IPの追加・削除を1箇所で完結させたい

設定

map $remote_addr $maintenance_bypass_ip {
default 0;
203.0.113.10 1;
}
map $http_x_forwarded_for $maintenance_bypass_xff {
default 0;
"~203\.0\.113\.10" 1;
}
map $uri $maintenance_bypass_uri {
default 0;
"~^/maintenance/" 1;
"/health_check.html" 1;
}
server {
# ...
error_page 503 /maintenance/maintenance.html;
location /maintenance/ {
root /data/var/www/html;
}
location / {
if (-e /var/tmp/do_maintenance) {
return 503;
}
}
}

除外条件はすべてmapのテーブルに寄せてある。location /の中に残っているのはフラグファイルの存在チェックだけで、これはmapではなくifで判定するしかない(-eのようなファイル存在チェックはmapの対象にできないため)。

mapにした理由

ifをlocationコンテキストの中で連鎖させる書き方は、ディレクティブの評価順序が他の設定と絡むと想定外の挙動を起こすことがある(“if is evil”としてよく知られている)。除外条件を足すたびにifを積んでいく設計だと、後から読んだときに上から順に条件を目で追わないと全体の挙動が分からなくなる。

mapならテーブルに1行足すだけで済むし、defaultに対する例外というシンプルな形で管理できる。

X-Forwarded-Forの判定は~で正規表現マッチにしている。プロキシを複数経由すると$http_x_forwarded_forは 203.0.113.10, 10.0.0.5 のようにカンマ区切りで連結されるので、=による完全一致だと途中に別のIPが挟まった時点でマッチしなくなる。今回は「含まれていればOK」という判定にしてあるが、先頭一致・末尾一致まで厳密にしたい場合は正規表現をもう少し詰める必要がある(実機未確認)。

判定フロー

flowchart TD
    A[リクエスト受信] --> B{do_maintenanceが存在?}
    B -- no --> Z[通常応答]
    B -- yes --> C{URIが/maintenance/ or /health_check.html?}
    C -- yes --> Z
    C -- no --> D{remote_addrが許可IP?}
    D -- yes --> Z
    D -- no --> E{XFFに許可IPを含む?}
    E -- yes --> Z
    E -- no --> F[503を返す]

除外対象を増やしたいときは、対応するmapのテーブルに1行足すだけでいい。location側のロジックを触る必要がないので、変更の影響範囲が分かりやすい。