Переменные, map и блок if

Рейтинг: 75.8% · 12 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Переменные, map и блок if

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if (вы здесь)
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Рано или поздно ты упрёшься в стену. Конфиг растёт, появляются "если такой Host - то так, если WebSocket - то иначе, если бот - режем лимит". Первое, что просится под руку - вложить десяток if внутрь location. И вот тут начинается боль: один if переопределяет другой, add_header молча исчезает, proxy_pass ведёт не туда, а ты сидишь и не понимаешь, почему конфиг "врёт". Эта глава - про то, как принимать решения в nginx правильно: через переменные и через map, а не через if. И про то, почему фраза "if is evil" родилась не на пустом месте.

Разберём по слоям: какие бывают переменные и чем коварно похожие пары вроде $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. Дефисы меняются на подчёркивания, регистр в нижний.
Практическая разница $uri и $request_uri видна сразу. Запрос GET /catalog/../news?id=7:

Код: Выделить всё

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
Если в редиректе нужно сохранить ровно то, что ввёл пользователь, - бери $request_uri. Если матчишь путь без параметров - $uri. Перепутаешь - получишь либо потерянные query-параметры, либо двойной ? в результирующем URL.

Свои переменные объявляются через 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;
}
Читается так: если клиент прислал заголовок Upgrade (любое непустое значение, обычно "websocket") - в $connection_upgrade кладём строку "upgrade". Если Upgrade пуст ('' - пустая строка), значит обычный HTTP - кладём "close", чтобы корректно завершить соединение. default срабатывает на всё, что не совпало явно. Дальше это используется при проксировании:

Код: Выделить всё

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}
Без map тут пришлось бы писать if, и оно бы развалилось (увидишь почему ниже). А с map одна таблица обслуживает и WebSocket, и обычный трафик, и переключение прозрачно.

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;
}
Ключевые правила, на которых спотыкаются: исходных переменных можно перечислить только ОДНУ. Если решение зависит от двух условий - делай цепочку map (выход одного становится входом другого) или сначала склей переменные. Значения с спецсимволами (пробелы, ;, {) бери в кавычки. Порядок проверки: точные совпадения, затем самый длинный wildcard-префикс/суффикс, затем регэкспы по порядку записи, затем default. Опция volatile отключает кеширование результата (нужно редко). Размер хеша рулится map_hash_max_size / map_hash_bucket_size - на огромных таблицах поднимай.
Важно: в эталонном gateway на цепочках map построена вся сквозная трассировка - $http_x_request_id превращается в $trace_request_id, дальше собирается цепочка хопов через несколько последовательных map. Это та же механика, что и с Upgrade, просто несколько таблиц подряд. Подробно разберём в главе про трассировку - сейчас держи в голове, что map масштабируется до сложной логики без единого if.
nginx geo и split_clients: продвинутое, можно вернуться позже

Рядом с 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 - делит трафик по проценту, детерминированно хешируя ключ (например $request_id или $remote_addr). Это рабочая лошадка A/B-тестов и canary-выкаток: один и тот же клиент всегда попадает в ту же группу, пока ключ стабилен.

Код: Выделить всё

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; }
Проценты должны давать в сумме не больше 100, остаток - под *. Меняешь 5% на 20% и перезагружаешь конфиг - доля канарейки растёт без правки кода приложения. Вот и весь канареечный деплой на голом nginx.

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 подряд с set - предсказуемы, но как только внутри if появляется add_header, proxy_pass или try_files, начинаются чудеса: add_header из родительского location может НЕ примениться, если сработал if со своим add_header (директивы add_header не наследуются в блок, где есть свои); proxy_pass внутри if в некоторых конфигурациях приводит к segfault или к тому, что запрос уходит не туда. Эмпирическое правило сообщества жёсткое: внутри location в if безопасны ровно две вещи - return ... и rewrite ... last|redirect|permanent. Всё остальное - рулетка.

Где 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;
}
А чем заменять if внутри location - этому и была посвящена вся глава. Памятка по замене:
  • Выбор значения по условию ("если бот - то 1") -> map. Почти всегда так.
  • Отдать файл, если существует, иначе фолбэк -> try_files $uri $uri/ =404, а не if (-f ...).
  • Решение по подсети/IP -> geo (плюс map поверх него).
  • Процентное разделение трафика -> split_clients.
  • Ограничение метода -> limit_except либо if с return на уровне server.
if (-f $request_filename) и if (!-e ...) - отдельный антипаттерн: это медленные stat-проверки на каждом запросе, и try_files делает то же самое быстрее и предсказуемее. Если видишь в чужом конфиге частокол if внутри location - это почти всегда переписывается на map плюс try_files, и конфиг становится и быстрее, и понятнее.

Мини-лаба: повтори руками

Соберём 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. Конфиг от этого становится и быстрее, и читаемее, и перестаёт "врать".
👍5 ❤️4 🔥1 😄 🤔2
Аватара пользователя
grpc88
Сообщения: 1
Зарегистрирован: 17 май 2026, 05:16

Re: Переменные, map и блок if

Сообщение grpc88 »

Спасибо, наконец-то дошло про $uri и $request_uri. Год редиректил через $uri и терял ?utm-метки, теперь понятно почему.
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
mpa3uh
Сообщения: 1
Зарегистрирован: 01 июн 2026, 09:59

Re: Переменные, map и блок if

Сообщение mpa3uh »

А цепочку из двух map можно? Хочу решать по Host И методу сразу, одной таблицей вроде нельзя - только один вход?
👍 ❤️ 🔥2 😄 🤔1
Ответить
← Предыдущая глава
Раздача статики: root, alias, try_files, sendfile
Следующая глава →
rewrite, return и канонизация URL

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Директива location в nginx: матчинг и приоритетыnginx rewrite и редиректы

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя