T-CREATOR

Nginx ディレクティブ早見表:server/location/if/map の評価順序と落とし穴

Nginx ディレクティブ早見表:server/location/if/map の評価順序と落とし穴

Nginx の設定ファイルを書いていると、「なぜこのディレクティブが効かないのだろう?」と悩んだ経験はありませんか。実は Nginx には明確な 評価順序 があり、それを理解していないと思わぬ挙動に遭遇してしまいます。

この記事では、Nginx の主要ディレクティブである serverlocationifmap の評価順序を整理し、実際に起こりがちな落とし穴とその対策について詳しく解説します。設定ファイルの理解を深めて、確実に意図した動作を実現できるようになりましょう。

Nginx ディレクティブ評価順序 早見表

#フェーズディレクティブ評価タイミング使用場所主な用途注意点
1設定読み込みmapNginx 起動時・reload 時http コンテキスト変数の条件付き変換静的評価。生成した変数はリクエストごとに評価される
2リクエスト受信serverHost ヘッダーマッチング時http コンテキストバーチャルホスト選択優先順位:完全一致 > ワイルドカード > 正規表現 > default_server
3server rewriterewrite (server 内)location 選択前server コンテキストURI 書き換え(全体)location マッチング前に実行される
4location 選択locationURI パターンマッチング時server コンテキストURI パターンごとの処理定義優先順位:= > ^~ > ~ / ~* > プレフィックス
5location rewriterewrite (location 内)location 確定後location コンテキストURI 書き換え(局所)last で再マッチング、break で継続
6rewrite フェーズif条件評価時server / location コンテキスト条件分岐return / rewrite / set のみ使用推奨。proxy_pass は不可
7content フェーズproxy_pass などレスポンス生成時location コンテキスト実際の処理実行if 内では使用不可

早見表の読み方

  • 評価タイミング:そのディレクティブがいつ評価されるかを示します
  • 使用場所:設定ファイルのどのコンテキストで記述できるかを示します
  • 主な用途:そのディレクティブの典型的な使用目的です
  • 注意点:実装時に注意すべき重要なポイントです

背景

Nginx は高性能な Web サーバー・リバースプロキシとして広く利用されていますが、その設定ファイル(nginx.conf)は 宣言的 に記述します。しかし、宣言的であるがゆえに「書いた順番通りに実行される」わけではなく、Nginx 内部で決められた 評価フェーズ に従って処理が進みます。

Nginx のリクエスト処理フロー

Nginx がリクエストを受け取ってからレスポンスを返すまでには、大きく分けて以下のフェーズがあります。

mermaidflowchart TD
  start["リクエスト受信"] --> server_select["server ブロック選択"]
  server_select --> rewrite_phase["rewrite フェーズ<br/>(server 内)"]
  rewrite_phase --> location_select["location マッチング"]
  location_select --> location_rewrite["rewrite フェーズ<br/>(location 内)"]
  location_rewrite --> access_phase["access フェーズ"]
  access_phase --> content_phase["content フェーズ"]
  content_phase --> response["レスポンス返却"]

この図からわかるように、Nginx は「どの server ブロックを選ぶか」→「rewrite で URI を書き換えるか」→「どの location にマッチするか」→「さらに location 内で処理」という流れで進みます。

主要ディレクティブの役割

#ディレクティブ役割評価タイミング
1serverバーチャルホスト単位の設定を定義リクエスト受信直後
2map変数の値を条件に応じて変換設定読み込み時(静的)
3rewriteURI を書き換えるrewrite フェーズ
4locationURI パターンごとの処理を定義location マッチングフェーズ
5if条件分岐を行うrewrite フェーズまたは content フェーズ

これらのディレクティブは、記述した順番ではなく フェーズごとに決められた順序 で評価されるため、意図しない動作を引き起こすことがあります。

課題

Nginx の設定でよく遭遇する課題は、「書いた順番通りに動かない」 ことです。特に以下のような問題が頻繁に発生します。

よくある誤解と問題

1. if ディレクティブの誤用

if は一見便利に見えますが、Nginx 公式ドキュメントでは 「if is evil(if は悪)」 と表現されるほど、予期しない挙動を引き起こします。

typescript// ❌ 誤った例:if で location を選ぼうとする
server {
  if ($request_uri ~ ^/api/) {
    proxy_pass http://backend;  // エラー!
  }
}

上記のコードは一見正しそうですが、if の中で proxy_pass は使えません。Nginx はこの設定でエラーを返します。

2. rewrite と location の評価順序の混乱

rewritelocation マッチングの に評価されるため、意図しない location にマッチしてしまうことがあります。

typescript// ❌ 誤った例:rewrite 後の location マッチング
location /old-path {
  rewrite ^/old-path/(.*)$ /new-path/$1 last;
}

location /new-path {
  proxy_pass http://backend;
}

このコードでは、​/​old-path​/​test へのアクセスは ​/​new-path​/​test に書き換わりますが、last フラグにより location マッチングが再実行 されます。

3. map の評価タイミングの誤解

map は設定ファイル読み込み時に評価されるため、リクエストごとに動的に変わる値を直接参照できません。

typescript// ❌ 誤った例:map 内で location の変数を使う
map $uri $backend {
  ~/api/  api-server;
  ~/web/  web-server;
}

一見正しそうですが、maphttp コンテキスト で定義し、変数を参照する形で使います。

問題が起こる理由

これらの問題が起こるのは、Nginx の 評価フェーズ を理解していないためです。以下の図で評価順序を確認しましょう。

mermaidflowchart LR
  config_load["設定ファイル読み込み"] --> map_eval["map 評価<br/>(静的)"]
  map_eval --> request["リクエスト受信"]
  request --> server_block["server ブロック選択"]
  server_block --> server_rewrite["server 内<br/>rewrite 実行"]
  server_rewrite --> location_match["location<br/>マッチング"]
  location_match --> location_rewrite["location 内<br/>rewrite 実行"]
  location_rewrite --> if_eval["if 評価"]
  if_eval --> content["content 処理"]

この順序を把握することで、「なぜ動かないのか」が明確になります。

解決策

Nginx のディレクティブを正しく使うには、評価フェーズごとに適切なディレクティブを選ぶ ことが重要です。以下、各ディレクティブの正しい使い方と評価順序を解説します。

評価順序の早見表

Nginx のディレクティブ評価順序を整理すると、以下のようになります。

#フェーズディレクティブ実行タイミング備考
1設定読み込み時mapNginx 起動時・reload 時静的な変数変換
2リクエスト受信serverHost ヘッダーでマッチバーチャルホスト選択
3server rewriterewrite(server 内)location 選択前URI 書き換え
4location マッチングlocationURI パターンでマッチ優先度あり
5location rewriterewrite(location 内)location 確定後再マッチング可能
6access フェーズif(一部)アクセス制御時条件付きアクセス制御
7content フェーズif(一部)、proxy_pass などレスポンス生成時実際の処理実行

1. server ブロックの評価

server ブロックは、リクエストの Host ヘッダー に基づいて選択されます。

server_name によるマッチング優先順位

typescript// server ブロックのマッチング優先順位
typescript// 1. 完全一致(最優先)
server {
  server_name example.com;
  # 完全に一致する場合に選択される
}
typescript// 2. ワイルドカード(前方)
server {
  server_name *.example.com;
  # サブドメインにマッチ
}
typescript// 3. ワイルドカード(後方)
server {
  server_name www.example.*;
  # TLD が異なる場合にマッチ
}
typescript// 4. 正規表現
server {
  server_name ~^www\d+\.example\.com$;
  # 複雑なパターンにマッチ
}
typescript// 5. default_server(最後の手段)
server {
  listen 80 default_server;
  server_name _;
  # どこにもマッチしない場合
}

重要なポイント: 複数の server ブロックがマッチした場合、上記の優先順位で 1 つだけ が選ばれます。

2. map ディレクティブの評価

map は設定ファイル読み込み時に評価される 静的なディレクティブ です。変数の値を条件に応じて変換します。

map の正しい使い方

typescript// map は http コンテキストで定義
typescripthttp {
  // リクエスト URI に応じてバックエンドを切り替える map
  map $request_uri $backend_server {
    ~^/api/     api.backend.com;
    ~^/static/  static.backend.com;
    default     default.backend.com;
  }

  // map で定義した変数は server や location で利用可能
  server {
    location / {
      proxy_pass http://$backend_server;
    }
  }
}

map の評価タイミング

typescript// map は設定読み込み時に一度だけ評価される
typescripthttp {
  // User-Agent に応じてモバイル判定
  map $http_user_agent $is_mobile {
    ~*android       1;
    ~*iphone        1;
    ~*ipad          1;
    default         0;
  }

  server {
    location / {
      // $is_mobile は各リクエストで再評価される
      if ($is_mobile) {
        rewrite ^ /mobile$request_uri last;
      }
    }
  }
}

重要なポイント: map 自体は静的ですが、map で生成した変数($is_mobile など)は リクエストごとに評価 されます。

3. rewrite ディレクティブの評価

rewrite は URI を書き換えるディレクティブで、server 内location 内 で評価タイミングが異なります。

server 内の rewrite(location マッチング前)

typescript// server 内の rewrite は location マッチング「前」に実行される
typescriptserver {
  // この rewrite は全 location より先に実行される
  rewrite ^/old-api/(.*)$ /api/$1 last;

  location /api/ {
    proxy_pass http://backend;
  }

  location /old-api/ {
    // ここには到達しない!
    return 404;
  }
}

上記の例では、​/​old-api​/​users へのアクセスは server 内の rewrite​/​api​/​users に書き換わり、location ​/​api​/​ にマッチします。location ​/​old-api​/​ には 到達しません

location 内の rewrite(location 確定後)

typescript// location 内の rewrite は location マッチング「後」に実行される
typescriptserver {
  location /api/ {
    // location 確定後に rewrite 実行
    rewrite ^/api/v1/(.*)$ /api/v2/$1 break;
    proxy_pass http://backend;
  }
}

rewrite フラグの違い

#フラグ動作用途
1lastlocation マッチングを再実行別の location に転送したい場合
2breakrewrite を終了し次の処理へ同じ location 内で完結させたい場合
3redirect302 リダイレクトクライアントに URL 変更を通知
4permanent301 リダイレクト恒久的な URL 変更
typescript// フラグによる動作の違い
typescriptlocation /test {
  // last: location マッチングをやり直す
  rewrite ^/test/(.*)$ /new/$1 last;
}

location /new {
  return 200 "Matched /new";
}
typescriptlocation /test {
  // break: rewrite 後、この location 内で処理継続
  rewrite ^/test/(.*)$ /new/$1 break;
  proxy_pass http://backend;  // /new/* として proxy される
}

4. location ディレクティブの評価

location は URI パターンにマッチする処理を定義します。複数の location がマッチした場合、優先順位 に従って 1 つが選ばれます。

location マッチングの優先順位

typescript// location のマッチング優先順位
typescriptserver {
  // 1. 完全一致(= で始まる)- 最優先
  location = /exact {
    return 200 "Exact match";
  }

  // 2. 優先プレフィックス(^~ で始まる)
  location ^~ /priority {
    return 200 "Priority prefix";
  }

  // 3. 正規表現(~ または ~* で始まる)- 記述順に評価
  location ~ \.php$ {
    return 200 "PHP file (case-sensitive)";
  }

  location ~* \.(jpg|png)$ {
    return 200 "Image file (case-insensitive)";
  }

  // 4. 通常のプレフィックス - 最長マッチ
  location /images/ {
    return 200 "Images directory";
  }

  location / {
    return 200 "Root";
  }
}

location マッチングの流れ

mermaidflowchart TD
  request["リクエスト URI"] --> exact_check["完全一致<br/>(=) チェック"]
  exact_check -->|マッチ| exact_done["確定"]
  exact_check -->|マッチせず| priority_check["優先プレフィックス<br/>(^~) チェック"]
  priority_check -->|マッチ| priority_done["確定"]
  priority_check -->|マッチせず| regex_check["正規表現<br/>(~, ~*) チェック"]
  regex_check -->|マッチ| regex_done["確定<br/>(最初にマッチしたもの)"]
  regex_check -->|マッチせず| prefix_check["通常プレフィックス<br/>最長マッチ"]
  prefix_check --> prefix_done["確定"]

重要なポイント: 正規表現 location は 記述順 に評価され、最初にマッチしたものが採用されます。通常のプレフィックスは記述順に関係なく 最長マッチ が選ばれます。

location マッチングの実例

typescript// 実際のマッチング例
typescriptserver {
  location = /test {
    return 200 "A: Exact";
  }

  location /test {
    return 200 "B: Prefix";
  }

  location ~ /test {
    return 200 "C: Regex";
  }
}

// /test → A (完全一致)
// /test/ → C (正規表現マッチ、Bより優先)
// /test/abc → C (正規表現マッチ)

5. if ディレクティブの評価

if は条件分岐を行うディレクティブですが、使用には注意が必要 です。Nginx 公式ドキュメントでも「if is evil」と警告されています。

if が安全に使える場所

typescript// ✅ return と組み合わせる場合は安全
typescriptserver {
  location / {
    if ($request_method = POST) {
      return 405;  // POST メソッドを拒否
    }

    if ($http_user_agent ~* (bot|crawler)) {
      return 403;  // ボットをブロック
    }
  }
}
typescript// ✅ rewrite と組み合わせる場合も比較的安全
typescriptserver {
  location / {
    if ($scheme = http) {
      return 301 https://$host$request_uri;
    }
  }
}

if で避けるべきパターン

typescript// ❌ if の中で proxy_pass を使う(動作しない)
typescriptserver {
  location / {
    if ($request_uri ~ ^/api/) {
      proxy_pass http://backend;  // エラー!
    }
  }
}
typescript// ✅ 正しい方法:location で分ける
typescriptserver {
  location /api/ {
    proxy_pass http://backend;
  }
}
typescript// ❌ if をネストする(予期しない動作)
typescriptlocation / {
  if ($request_method = POST) {
    if ($http_content_type ~ json) {
      // ネストした if は避ける
    }
  }
}
typescript// ✅ 正しい方法:map で変数を作る
typescripthttp {
  map "$request_method:$http_content_type" $is_json_post {
    ~^POST:.*json.*  1;
    default          0;
  }

  server {
    location / {
      if ($is_json_post) {
        return 200 "JSON POST request";
      }
    }
  }
}

if の評価タイミング

ifrewrite フェーズ で評価されますが、内部で使えるディレクティブには制限があります。

#ディレクティブif 内で使用可能か
1return✅ 可能
2rewrite✅ 可能
3set✅ 可能
4proxy_pass❌ 不可(エラー)
5try_files❌ 不可(無視される)
6add_header⚠️ 動作が不安定

評価順序のまとめ

すべてのディレクティブを組み合わせた場合の評価順序を確認しましょう。

mermaidflowchart TD
  start["Nginx 起動"] --> map_load["map 評価<br/>(静的)"]
  map_load --> request_receive["リクエスト受信"]
  request_receive --> server_select["server ブロック選択<br/>(server_name マッチング)"]
  server_select --> server_rewrite["server 内 rewrite<br/>(location 選択前)"]
  server_rewrite --> location_find["location マッチング<br/>(優先順位順)"]
  location_find --> location_rewrite["location 内 rewrite"]
  location_rewrite --> if_check["if 条件評価<br/>(rewrite フェーズ)"]
  if_check --> access_phase["access フェーズ"]
  access_phase --> content_phase["content フェーズ<br/>(proxy_pass など)"]
  content_phase --> response["レスポンス返却"]

この流れを理解することで、「なぜこのディレクティブが効かないのか」「どの順番で評価されるのか」が明確になります。

具体例

実際のユースケースを通じて、評価順序を意識した正しい設定方法を見ていきましょう。

例 1:API とフロントエンドのルーティング

Next.js などの SPA と API を同じドメインで運用する場合、パスによって転送先を変える必要があります。

誤った実装例

typescript// ❌ 誤った例:if で分岐しようとする
typescriptserver {
  listen 80;
  server_name example.com;

  location / {
    if ($request_uri ~ ^/api/) {
      proxy_pass http://api-backend;  // エラー!
    }
    proxy_pass http://frontend;
  }
}

この設定は エラーになりますif の中で proxy_pass は使えません。

正しい実装例

typescript// ✅ 正しい例:location でパスを分ける
typescriptserver {
  listen 80;
  server_name example.com;

  // API リクエストは API サーバーへ
  location /api/ {
    proxy_pass http://api-backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }

  // それ以外はフロントエンドへ
  location / {
    proxy_pass http://frontend;
    proxy_set_header Host $host;
  }
}

さらに複雑な条件分岐が必要な場合

typescript// map を使って条件に応じたバックエンドを選択
typescripthttp {
  // リクエスト URI とメソッドに応じてバックエンドを決定
  map "$request_uri:$request_method" $backend {
    ~^/api/.*:POST      api-write.backend.com;
    ~^/api/.*:GET       api-read.backend.com;
    ~^/admin/.*         admin.backend.com;
    default             frontend.backend.com;
  }

  server {
    listen 80;
    server_name example.com;

    location / {
      proxy_pass http://$backend;
    }
  }
}

上記の設定では、map で変数 $backend を生成し、その値を proxy_pass で利用しています。これにより複雑な条件分岐を 安全に 実現できます。

例 2:モバイル向けリダイレクト

User-Agent に基づいてモバイル用ページへリダイレクトする場合の実装です。

誤った実装例

typescript// ❌ 誤った例:location 内の if で複雑な処理
typescriptserver {
  location / {
    if ($http_user_agent ~* (android|iphone)) {
      if ($request_uri !~ ^/mobile/) {
        rewrite ^ /mobile$request_uri permanent;
      }
    }
  }
}

ネストした if は予期しない動作を引き起こす可能性があります。

正しい実装例

typescript// ✅ 正しい例:map で変数を作り、if を単純化
typescripthttp {
  // モバイル判定
  map $http_user_agent $is_mobile {
    ~*android       1;
    ~*iphone        1;
    ~*ipad          1;
    default         0;
  }

  // モバイルかつ /mobile/ 以外のパス
  map "$is_mobile:$request_uri" $should_redirect {
    ~^1:(?!/mobile/).*   1;
    default              0;
  }

  server {
    listen 80;
    server_name example.com;

    location / {
      // 単純な if で済む
      if ($should_redirect) {
        return 301 /mobile$request_uri;
      }

      root /var/www/html;
      index index.html;
    }

    location /mobile/ {
      root /var/www/mobile;
      index index.html;
    }
  }
}

この実装では、map を使って複雑な条件を事前に計算し、if では単純な判定のみを行います。

例 3:レガシー URL のリライト

古い URL 構造から新しい構造へリライトする場合、rewrite の評価順序が重要になります。

誤った実装例

typescript// ❌ 誤った例:location 内の rewrite で別 location を期待
typescriptserver {
  location /old-product/ {
    rewrite ^/old-product/(.*)$ /product/$1 last;
    return 404;  // ここに到達しないと思いきや...
  }

  location /product/ {
    proxy_pass http://backend;
  }
}

この設定では、rewritelast フラグにより location マッチングが再実行されますが、同じ location 内の return 404 も実行される可能性 があります。

正しい実装例:server 内で rewrite

typescript// ✅ 正しい例:server 内で rewrite し、location マッチングを再実行
typescriptserver {
  // server 内の rewrite は location 選択「前」に実行される
  rewrite ^/old-product/(.*)$ /product/$1 last;
  rewrite ^/legacy/(.*)$ /new/$1 last;

  location /product/ {
    proxy_pass http://backend;
  }

  location /new/ {
    proxy_pass http://new-backend;
  }

  // /old-product//legacy/ の location は不要
}

server 内で rewrite を行うことで、location マッチング前 に URI を書き換えられます。

正しい実装例:map でパターンマッチング

typescript// ✅ 正しい例:map で URL マッピングを定義
typescripthttp {
  // 旧 URL から新 URL へのマッピング
  map $request_uri $new_uri {
    ~^/old-product/(.*)$   /product/$1;
    ~^/legacy/(.*)$        /new/$1;
    ~^/archive/(.*)$       /articles/$1;
    default                "";
  }

  server {
    listen 80;
    server_name example.com;

    location / {
      // マッピングが存在すればリダイレクト
      if ($new_uri != "") {
        return 301 $new_uri;
      }

      proxy_pass http://backend;
    }
  }
}

この方法では、URL マッピングを map で一元管理でき、メンテナンス性が向上します。

例 4:複数条件による Access 制御

IP アドレスと User-Agent の両方をチェックしてアクセス制御を行う場合です。

誤った実装例

typescript// ❌ 誤った例:ネストした if
typescriptlocation /admin/ {
  if ($remote_addr != 192.168.1.100) {
    if ($http_user_agent !~ admin-tool) {
      return 403;
    }
  }
}

正しい実装例

typescript// ✅ 正しい例:map で条件を統合
typescripthttp {
  // IP アドレスチェック
  map $remote_addr $is_trusted_ip {
    192.168.1.100   1;
    10.0.0.0/8      1;
    default         0;
  }

  // User-Agent チェック
  map $http_user_agent $is_admin_tool {
    ~admin-tool     1;
    default         0;
  }

  // 両方の条件を統合
  map "$is_trusted_ip:$is_admin_tool" $allow_admin {
    ~^1:.*    1;  // 信頼できる IP なら許可
    ~^.*:1$   1;  // admin ツールなら許可
    default   0;  // それ以外は拒否
  }

  server {
    location /admin/ {
      if ($allow_admin = 0) {
        return 403;
      }

      proxy_pass http://admin-backend;
    }
  }
}

この実装では、複数の条件を map で段階的に評価し、最終的に 1 つの変数($allow_admin)で判定します。

例 5:動的バックエンドの選択

リクエストヘッダーや Cookie に基づいて、異なるバックエンドサーバーへ転送する場合です。

A/B テストの実装例

typescript// Cookie に基づいて A/B テストのバックエンドを切り替え
typescripthttp {
  // Cookie の ab_test 値をチェック
  map $cookie_ab_test $backend_version {
    "version_a"   backend-a.internal;
    "version_b"   backend-b.internal;
    default       backend-a.internal;
  }

  server {
    listen 80;
    server_name example.com;

    location / {
      proxy_pass http://$backend_version;
      proxy_set_header Host $host;

      // Cookie がない場合はランダムに割り当て
      if ($cookie_ab_test = "") {
        add_header Set-Cookie "ab_test=version_a; Path=/; Max-Age=86400";
      }
    }
  }
}

カスタムヘッダーによるルーティング

typescript// カスタムヘッダー X-Backend-Version でバックエンドを選択
typescripthttp {
  map $http_x_backend_version $backend {
    "v1"      backend-v1.internal;
    "v2"      backend-v2.internal;
    "canary"  backend-canary.internal;
    default   backend-v1.internal;
  }

  server {
    listen 80;
    server_name api.example.com;

    location /api/ {
      proxy_pass http://$backend;
      proxy_set_header X-Original-Backend $backend;
    }
  }
}

実装のベストプラクティス

これらの例から、以下のベストプラクティスが見えてきます。

#プラクティス理由
1if の代わりに location を使う評価順序が明確で、予期しない動作を避けられる
2複雑な条件は map で事前計算if のネストを避け、可読性が向上する
3rewriteserver 内に配置location マッチング前に URI を変換できる
4if 内では returnrewrite のみproxy_pass などは使えないため
5変数を活用して条件を統合複数の map を組み合わせて柔軟な制御を実現

まとめ

Nginx のディレクティブには明確な 評価順序 があり、それを理解することが正しい設定の第一歩です。

評価順序の要点

  1. map:設定読み込み時に評価される(静的)
  2. server:Host ヘッダーでバーチャルホストを選択
  3. server 内 rewrite:location マッチング「前」に URI を書き換え
  4. location:URI パターンでマッチング(優先順位あり)
  5. location 内 rewrite:location 確定「後」に URI を書き換え
  6. if:rewrite フェーズで評価(使用は最小限に)

落とし穴を避けるためのチェックリスト

  • ifreturn または rewrite とのみ組み合わせる
  • proxy_pass などは location で分ける
  • ✅ 複雑な条件は map で変数化する
  • rewritelastbreak フラグを使い分ける
  • location の優先順位(完全一致 > 優先プレフィックス > 正規表現 > 通常プレフィックス)を意識する
  • server 内 rewrite は location マッチング前に実行されることを理解する

これらのポイントを押さえることで、Nginx の設定ファイルを確実に、そして意図した通りに動作させることができます。評価順序を理解し、適切なディレクティブを選ぶことで、複雑なルーティングやアクセス制御も安全に実装できるでしょう。

関連リンク