🐠

NGINXのserver_nameは完全一致→ワイルドカード→正規表現の順番で探索される

2021/04/24に公開

やりたかったこと

*.example.com へのリクエストは当該ドメインの3000ポートに流したい、ただし user01-test.example.com というような形式へのリクエストだけは3001ポートに流したい
という要件があったのでNGINXのバーチャルサーバーを2つ用意し、server_nameにワイルドカードのものと正規表現のものをそれぞれ設定してみましたが、すべてのリクエストが3000ポートに流れてしまい、正規表現ルールが評価されませんでした。

server {
  listen 80;
  server_name ~^([a-zA-Z0-9]+)-test\.example\.com$;

  location / {
    proxy_set_header   Host $http_host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Real-IP $remote_addr;

    proxy_pass http://$http_host:3001;
  }
}

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

  location / {
    proxy_set_header   Host $http_host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Real-IP $remote_addr;

    proxy_pass http://$http_host:3000;
  }
}

なぜ正規表現が評価されないのか

NGINXのバーチャルサーバーの仕組みはリクエストヘッダー内のHostフィールドとserver_name が一致した場合、該当するserverの設定が評価されるようになっています。Hostフィールドの値とマッチするserver_nameが複数存在する場合は下記の優先順位のうち最初にマッチするserverの設定が評価されることになっているようです(※)。このルールを見ると正規表現の優先順位はワイルドカードより後のため、正規表現で書いたバーチャルサーバーの設定が評価されないことがわかりました。

  1. 完全一致
  2. アスタリスクで始まる最長のワイルドカード名(例:*.example.org)
  3. アスタリスクで終わる最も長いワイルドカード名(例:mail.*)
  4. 最初にマッチする正規表現

(※)NGINXの公式ドキュメントより
http://nginx.org/en/docs/http/server_names.html

なぜ正規表現は最後に評価されるのか

NGINXのserver_nameのドキュメントに最適化に関する記述があり、これを読むと計算量の観点から探索処理が早く終了する順序で評価をしているようです。
完全一致とワイルドカードはO(1)、正規表現はO(n)かなと思います。

解決手段

おまけとして やりたかったこと をどのように解決したのかを記載しておきます。
3001ポートへのリクエストは *.test.example.com に流してもらうように変更しました。こうすることでサブドメインが切られ、ワイルドカードを使ってserver_nameを指定することができるようになったので、正規表現を使う必要がなくなりました。

server {
  listen 80;
-  server_name ~^([a-zA-Z0-9]+)-test\.example\.com$;
+  server_name *.test.example.com;

  location / {
    proxy_set_header   Host $http_host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Real-IP $remote_addr;

    proxy_pass http://$http_host:3001;
  }
}

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

  location / {
    proxy_set_header   Host $http_host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Real-IP $remote_addr;

    proxy_pass http://$http_host:3000;
  }
}

Discussion