PunchNJC suspends three judges, disciplines six othersThe Jerusalem PostPalestinian delegation seeks resolutions critical of Israel at UN General Assembly - report한겨레삼성바이오 부분파업 돌입…법원 요건 맞춰 전면파업은 3주 뒤 예고Daily MaverickNew wave of bomb hoaxes hits dozens of flights across EuropeZDF heuteEntdecken Sie das ZDF-NachrichtenstudioVarietyPriyanka Chopra Jonas, Russell Crowe Sci-Fi Thriller ‘Bluefly’ Adds ‘Beast’ Star Daniel MacPherson (EXCLUSIVE)سكاي نيوز عربيةهيغسيث يدافع عن خطته لإعادة هيكلة البنتاغون ويعلق عن "عزله"Seeking AlphaGEO forecasts Q4 2026 revenue $777M-$827M with adjusted EBITDA $149M-$154M as ICE activations expandDaily MailBondi Beach massacre death toll rises to 16: Survivor dies from complications resulting from their injuries almost eleven months laterSDP EspectáculosFrida Sofía acusa a Alejandra Guzmán de proteger a Enrique Guzmán pese a su denunciaNew Straits TimesSarawak polls set for Dec 12Globo EsporteCom golaço de Kaio Jorge, Cruzeiro vira sobre o Bahia e fica perto de confirmar vaga na Libertadores
The Daily Newsstand · Free, Always
Friday, November 6, 2026

Контейнер healthy, а релиз не проходит: как проверка Host сломала readiness

Translate

После ограничения допустимых host заголовков у меня перестал активироваться новый релиз. PHP-FPM запускался, контейнер получал статус healthy, Nginx успешно проверял конфигурацию, но deploy-скрипт сообщал, что кандидат не готов. Предыдущая версия продолжала обслуживать сайт.

В итоге исправлять пришлось не приложение, а HTTP запрос, которым проверялась его готовность. Ниже разберу, почему проверка по внутреннему адресу перестала быть эквивалентна обращению к сайту и как отличить эту ситуацию от действительно неработающего приложения. Доменные имена заменены на example.com.

Что изменилось перед сбоем

В моей конфигурации внешний Nginx принимает публичные запросы, а приложение работает в Docker. При blue-green деплое сначала запускается новый HTTP слот. Проверка обращается к нему внутри инфраструктуры, и только после успешного результата gateway переключает трафик.

До этого я обнаружил, что запрос с посторонним доменным именем может попасть в приложение. Причина такого поведения бывает вполне прозаической: Nginx выбирает виртуальный сервер по адресу, порту и имени из запроса. Если подходящего имени нет, запрос получает сервер по умолчанию для этого listener. Запись server_name сама по себе не является универсальным запретом на остальные имена. Узнал про посторонний домен на моем IP из поиска гугла)

Я добавил ограничения на неизвестные Host и SNI. После этого запросы к публичному домену продолжили работать, но внутренний запрос readiness стал отклоняться. Изменение закрыло лишний путь к приложению и одновременно выявило неверное предположение в deploy скрипте.

Почему healthy не означало ready

Проверка контейнера и проверка HTTP слота отвечали на разные вопросы. Проверка доступности сокета PHP-FPM могла подтвердить, что процесс принимает соединения. Она ничего не говорила о том, какой HTTP-запрос пропустит Nginx и какой Host примет приложение.

Полезно различать три уровня:

  • процесс запущен и слушает свой порт;

  • новый слот обслуживает ожидаемый HTTP запрос;

  • публичный маршрут через gateway, TLS и внешний прокси работает для пользователя.

У меня первый уровень проходил, второй перестал проходить после изменения правил, а третий продолжала обеспечивать предыдущая версия. Поэтому сочетание «контейнер healthy» и «кандидат не готов» не было противоречием. Это были результаты разных проверок.

Внутренний адрес меняет смысл запроса

Пусть внутри Docker новый веб-сервер доступен как web-blue. Команды ниже выполняются из диагностического контейнера с curl в той же Docker-сети; на обычном хосте такое имя разрешаться не обязано.

curl -i --max-time 5 http://web-blue/health

В этом запросе клиент по умолчанию использует web-blue как HTTP Host. Это удобное служебное имя для подключения, но оно не является публичным именем приложения. После появления ограничений такой запрос может получить отказ, хотя само приложение полностью готово обслужить example.com.

Для сравнения нужно оставить тот же адрес подключения и явно передать ожидаемый Host:

curl -i --max-time 5 \
  -H 'Host: example.com' \
  http://web-blue/health

Теперь обе команды обращаются к одному слоту, но представляют разные HTTP запросы. Если первая отклоняется, а вторая получает ожидаемый ответ, это явный признак проблемы с выбором виртуального хоста. Одного такого сравнения недостаточно, чтобы признать весь релиз исправным, но оно позволяет сузить поиск.

Сам /health в этих командах — маршрут приложения. Его контракт должен быть определён заранее. Статический файл со строкой ok не проверяет работу PHP и зависимостей. А endpoint, который проверяет каждую внешнюю интеграцию, может блокировать релиз из-за постороннего сбоя. Состав readiness проверки зависит от того, без чего конкретно этот HTTP слот не сможет обслуживать запросы.

Как я локализовал проблему

В такой ситуации я сначала разделяю проверку конфигурации, процессы и ответы HTTP. Успешный nginx -t означает корректность конфигурации для Nginx, но не гарантирует, что прикладной запрос получит статус 200.

Затем сравниваю запросы к одному кандидату с разными Host. Здесь важно не переключиться случайно на публичный домен: он может вести через gateway к старой версии и вернуть совершенно корректный ответ. Такой ответ докажет доступность старого релиза, а не готовность кандидата.

Последний шаг — проследить путь Host через прокси. Даже если curl отправил правильный заголовок, следующий Nginx может заменить его служебным именем upstream. Если ограничения есть и в Nginx, и в приложении, проверять нужно обе точки. При этом доверять любым Forwarded-заголовкам от внешнего клиента без настройки доверенных прокси нельзя.

В моём случае причина находилась в идентичности проверочного запроса. Наращивание таймаута и дополнительные перезапуски не исправляли бы запрос, который не соответствует разрешённому имени приложения.

Что изменилось в readiness-проверке

Ожидаемый Host теперь берётся из конфигурации публичного URL приложения. Адрес подключения при этом остаётся внутренним адресом конкретного кандидата. Служебные имена Docker не пришлось добавлять в публичный список разрешённых доменов.

Перед отправкой запроса проверяются значения, из которых он строится: схема, hostname и допустимый внутренний адрес назначения. В заголовок нельзя просто скопировать непроверенную строку конфигурации. Если для внутреннего HTTP-пути нужен Basic Auth, credentials передаются отдельно и не выводятся в диагностические сообщения.

Проверка должна подтверждать не только HTTP-статус, но и ожидаемый ответ health endpoint. Редирект не стоит автоматически считать успехом: он способен увести запрос с кандидата на публичный адрес или страницу входа. Слишком общий признак вроде «в ответе встретилась строка ok» тоже допускает ложные срабатывания.

После исправления у проверки появилась понятная формулировка: “конкретный новый слот отвечает на прикладной HTTP запрос с ожидаемым Host”. Публичная проверка после переключения остаётся отдельным этапом, потому что внутренний запрос не покрывает весь внешний маршрут.

Если проверяется HTTPS, одного Host недостаточно

В HTTP имя Host передаётся уже внутри запроса. В HTTPS до этого происходит TLS handshake, во время которого используется SNI и проверяется сертификат. Поэтому замена одного HTTP-заголовка не делает HTTPS запрос по IP эквивалентным запросу к домену.

Для проверки HTTPS на конкретном адресе удобно использовать --resolve. Например, если диагностический HTTPS listener доступен на 127.0.0.1:443 и имеет сертификат для example.com:

curl --fail --show-error --max-time 5 \
  --resolve example.com:443:127.0.0.1 \
  https://example.com/health

В URL сохраняется доменное имя, а подключение направляется на указанный IP. Оно используется для Host, SNI и проверки имени в сертификате. Адрес в --resolve нужно выбирать по своей схеме; в blue-green конфигурации это должен быть нужный кандидат или отдельный диагностический listener, а не случайно текущий публичный gateway.

Отключение проверки сертификата через -k может быть полезно только для отдельной диагностики и не должно незаметно становиться условием успешного деплоя. Иначе проверка начнёт пропускать другой класс ошибок.

Что стоит проверить после изменения Host защиты

Я бы включил в проверку конфигурации минимум четыре случая: разрешённый Host проходит к приложению; посторонний Host отклоняется; кандидат проверяется с ожидаемым именем; неготовый кандидат не получает публичный трафик. Для HTTPS отдельно добавляются сочетания правильного и неправильного SNI с Host.

Это можно воспроизвести небольшими интеграционными тестами на диагностическом приложении. Важно проверять ответы и маршрут до конкретного слота, а не только успешный запуск контейнеров. В журнале deploy скрипта достаточно сохранять этап, проверяемый слот, HTTP статус или тип ошибки - без паролей и токенов.

В этой истории blue-green выполнил свою задачу: пока новый слот не прошёл readiness, пользователи оставались на старом. Ошибкой оказалось предположение, что к приложению можно обратиться по любому внутреннему имени и получить тот же результат, что при запросе к публичному домену. После изменения маршрутизации, Host защиты или TLS это предположение стоит проверить заново.

P. S. Не судите строго, так как я бэкенд разработчик, а не девопс. Статья написана исключительно с целью предостеречь от детских ошибок при работе с blue-green деплоем.

Документация

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.