Разберём по слоям: какие бывают переменные и чем коварно похожие пары вроде $uri и $request_uri; как map превращает ветвления в быстрый хеш-лукап; где if всё-таки безопасен, а где гарантированно выстрелит в ногу. Поехали от простого к злому.
Nginx переменные: что у тебя реально в руках
Переменная в nginx - это не переменная из языка программирования. Большинство встроенных переменных ленивые: их значение вычисляется в момент, когда к ним обращаются, и кешируется на время обработки запроса. Ты не присваиваешь им значения (кроме своих, через set/map/geo) - ты их читаешь. Имя всегда начинается с $.
Самая частая ловушка - перепутать похожие пары. Запомни их раз и навсегда:
- $uri - нормализованный путь запроса БЕЗ query-строки. Декодирован (%20 уже превратился в пробел), слеши схлопнуты, ".." разрешены. Именно по $uri идёт выбор location. Меняется при internal rewrite.
- $request_uri - исходный URI как пришёл от клиента, ЦЕЛИКОМ, вместе с ?args и в сыром виде. Не нормализуется и не меняется при rewrite.
- $args (он же $query_string) - всё, что после ?. $arg_NAME - конкретный параметр, например $arg_token.
- $host - имя хоста в нижнем регистре: из строки запроса, иначе из заголовка Host, иначе из server_name. Всегда что-то есть.
- $http_host - сырой заголовок Host как прислал клиент (может быть пустым, может быть с портом, в любом регистре). Доверять ему опасно.
- $scheme - http или https.
- $request_method - GET, POST, HEAD и т.д.
- $remote_addr - IP того, кто открыл TCP-соединение. ВНИМАНИЕ: за прокси/CDN это адрес прокси, а не клиента (чинится модулем realip - отдельная глава).
- $http_NAME - любой заголовок запроса: $http_user_agent, $http_x_request_id, $http_referer. Дефисы меняются на подчёркивания, регистр в нижний.
Код: Выделить всё
location /debug {
add_header X-Uri $uri always;
add_header X-Request-Uri $request_uri always;
add_header X-Args $args always;
return 200 "ok\n";
}
# X-Uri: /news (нормализован, без args)
# X-Request-Uri: /catalog/../news?id=7 (как пришло)
# X-Args: id=7
Свои переменные объявляются через set внутри location/server, но set - это директива rewrite-модуля, и про её фазовые сюрпризы мы поговорим в конце. Чаще всего значение по условию правильнее задавать через map.

Nginx map: ветвления без боли, начнём с WebSocket
map - это, без преувеличения, самый полезный инструмент для условной логики в nginx. Он объявляется на уровне http (вне server!) и говорит: "возьми значение исходной переменной, найди по таблице соответствие и положи результат в новую переменную". Лукап - это хеш-таблица, собранная при загрузке конфига, поиск точного совпадения O(1) независимо от числа строк. Вычисление ленивое: пока к целевой переменной не обратятся, map не сработает.
Канонический пример, который ты увидишь в любом серьёзном конфиге, - проксирование WebSocket. Протокол требует апгрейда соединения через заголовки Upgrade и Connection. Но Connection - hop-by-hop заголовок, его нельзя слепо пробросить как есть. Решение из эталонного gateway:
Код: Выделить всё
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Код: Выделить всё
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
map умеет больше, чем точные совпадения. Несколько важных деталей синтаксиса:
Код: Выделить всё
map $http_user_agent $is_bot {
default 0;
"~*bot|crawl|spider" 1; # ~* регэксп, регистронезависимо
"~Googlebot" 1; # ~ регэксп, с учётом регистра
"Mozilla/5.0" 0; # точное совпадение
}
map $request_method $is_write {
hostnames; # для матча по поддоменам (тут не нужно, для примера)
POST 1;
PUT 1;
PATCH 1;
DELETE 1;
default 0;
}
nginx geo и split_clients: продвинутое, можно вернуться позжеВажно: в эталонном gateway на цепочках map построена вся сквозная трассировка - $http_x_request_id превращается в $trace_request_id, дальше собирается цепочка хопов через несколько последовательных map. Это та же механика, что и с Upgrade, просто несколько таблиц подряд. Подробно разберём в главе про трассировку - сейчас держи в голове, что map масштабируется до сложной логики без единого if.
Рядом с map живут два его родственника. Помечаю сразу: это продвинутая тема, новичку достаточно знать, что они есть - вернёшься, когда понадобится.
geo - тот же map, но вход всегда IP-адрес (по умолчанию $remote_addr), а таблица - это подсети. Удобно раздавать значение по географии/сети:
Код: Выделить всё
geo $is_internal {
default 0;
10.0.0.0/8 1;
172.16.0.0/12 1;
192.168.0.0/16 1;
}
# дальше $is_internal используешь в map для rate-limit, в if для return 403 и т.п.
Код: Выделить всё
split_clients "${remote_addr}${http_user_agent}" $variant {
5% "canary"; # 5% на новую версию
* "stable";
}
server {
location / {
proxy_pass http://$variant; # backend-пул выбирается по проценту
}
}
upstream canary { server 10.0.0.50:9000; }
upstream stable { server 10.0.0.10:9000; }
Nginx if: честно про "if is evil" и где он безопасен
Теперь о больном. Директива if в nginx - часть rewrite-модуля и выполняется в фазе rewrite. Проблема в том, что if внутри location создаёт фактически вложенный неявный конфиг-контекст, и взаимодействие с директивами других модулей даёт неинтуитивные, иногда катастрофические результаты. Это официально задокументировано на wiki nginx под заголовком "If is evil". Пример, который ломает мозг:
Код: Выделить всё
# ПЛОХО. Так делать нельзя.
location /bad {
set $a 1;
if ($arg_x) { set $a 2; }
if ($arg_y) { set $a 3; }
add_header X-First always; # установлен в одном if
if ($http_cookie ~* "vip") {
add_header X-Second always; # этот if "съедает" наследование
}
proxy_pass http://backend; # внутри if proxy_pass часто ломается
}
Где if реально безопасен и уместен - это на уровне server (не внутри location), для return и rewrite. Классика - редирект на https или отсечение по методу:
Код: Выделить всё
server {
listen 80;
server_name example.com;
# безопасно: только return на уровне server
if ($host = "www.example.com") {
return 301 https://example.com$request_uri;
}
return 301 https://example.com$request_uri;
}
- Выбор значения по условию ("если бот - то 1") -> map. Почти всегда так.
- Отдать файл, если существует, иначе фолбэк -> try_files $uri $uri/ =404, а не if (-f ...).
- Решение по подсети/IP -> geo (плюс map поверх него).
- Процентное разделение трафика -> split_clients.
- Ограничение метода -> limit_except либо if с return на уровне server.
Мини-лаба: повтори руками
Соберём WebSocket-map и эхо-переменных, чтобы пощупать всё вживую.
- Шаг 1. В http-блоке (вне server) добавь карту: map $http_upgrade $connection_upgrade { default upgrade; '' close; } и вторую: map $request_method $is_write { default 0; POST 1; PUT 1; DELETE 1; }
- Шаг 2. В server создай диагностический location:
Код: Выделить всё
location = /vars { default_type text/plain; add_header X-Uri $uri always; add_header X-Request-Uri $request_uri always; add_header X-Host $host always; add_header X-Http-Host $http_host always; add_header X-Is-Write $is_write always; return 200 "scheme=$scheme method=$request_method args=$args\n"; } - Шаг 3. Проверь синтаксис: nginx -t, затем перезагрузи: nginx -s reload.
- Шаг 4. Дёрни запросы и сравни заголовки ответа:
Код: Выделить всё
curl -i "http://localhost/vars?id=7" curl -i -X POST "http://localhost/vars/../vars?a=1&b=2" curl -i -H "Host: WWW.Example.COM:8080" "http://localhost/vars" - Шаг 5. Убедись: X-Uri нормализован и без args, X-Request-Uri - сырой; для POST X-Is-Write равен 1; X-Host в нижнем регистре, X-Http-Host - ровно как в заголовке (с портом и регистром).
- Шаг 6. Бонус: добавь split_clients "${remote_addr}" $variant { 50% A; * B; } и отдай $variant в ответе - увидишь, что для твоего IP вариант стабилен между запросами.
- Запрос пришёл как /a/../b?x=1. Что вернут $uri, $request_uri и $args по отдельности и почему?
- Почему map объявляется в http-блоке, а не внутри server, и в какой момент реально вычисляется его результат?
- Назови два (и только два) типа директив, которые безопасно держать внутри if внутри location. Что произойдёт с add_header, если положить его в if?
- Тебе нужно пускать 10% трафика на новую версию бэкенда так, чтобы один и тот же пользователь не прыгал между версиями. Какой инструмент возьмёшь и почему не if?
Переменные nginx - это твои датчики на запросе, и половина ошибок новичков растёт из путаницы $uri/$request_uri и $host/$http_host. map - основной инструмент условной логики: быстрый, наследуемый, масштабируемый до сложных цепочек (на них же построена трассировка). geo и split_clients - его специализированные родственники для IP и процентов. А if честно опасен внутри location: держи его на уровне server только для return/rewrite, а всю остальную логику переноси на map, try_files, geo и split_clients. Конфиг от этого становится и быстрее, и читаемее, и перестаёт "врать".