Merge pull request 'fix(ops/metrics): правки конфига Alertmanager молча не доезжали до контейнера' (#3142) from fix/3xxx-alertmanager-stale-inode into main
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / server (push) Successful in 18s
Deploy Metrics / agent-apps (push) Successful in 24s
Deploy Metrics / agent-infra (push) Successful in 24s
Deploy / build-backend (push) Successful in 39s
Deploy / build-worker (push) Successful in 40s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 11s

This commit is contained in:
lekss361 2026-08-27 12:29:54 +00:00
commit 9e37262ea0
2 changed files with 111 additions and 0 deletions

View file

@ -205,6 +205,10 @@ jobs:
exit 1
fi
echo "Алерты: канал задан, конфиг проверен, Alertmanager поднимается."
# Конфиг перерисован — значит у файла НОВЫЙ инод (см. rm выше).
# Помечаем, чтобы ниже пересоздать контейнер: почему это
# обязательно — объяснено у самого пересоздания.
ALERTMANAGER_RERENDERED=1
else
echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078."
fi
@ -219,6 +223,31 @@ jobs:
COMPOSE_PROFILES="$PROFILES" \
docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans
# ── Alertmanager: пересоздать, если конфиг перерисовали ─────────
# `up -d` выше СЧИТАЕТ alertmanager неизменившимся: он сравнивает
# описание сервиса, а содержимое бинд-маунта в это сравнение не
# входит. Контейнер продолжает работать — и продолжает держать
# СТАРЫЙ инод файла: `rm` при рендере не правит файл на месте, а
# создаёт новый, и открытый дескриптор внутри контейнера смотрит
# на прежний, уже удалённый.
#
# Отказ полностью беззвучный и оттого злой. На диске лежит новый
# конфиг, `amtool check-config` его проверяет и одобряет, деплой
# зелёный — а маршрутизация работает по старому. Пойман на проде
# 27.08: после #3136 на диске уже стоял `webhook_configs` на
# alert-ack, а контейнер всё ещё слал напрямую в Telegram. Значит
# и прежние правки маршрутов доезжали лишь тогда, когда контейнер
# пересоздавался по другой причине.
#
# Перезагрузка по SIGHUP/API не помогает: она перечитывает тот же
# открытый инод. Помогает только пересоздание контейнера.
if [ "${ALERTMANAGER_RERENDERED:-0}" = "1" ]; then
COMPOSE_PROFILES="$PROFILES" \
docker compose -p gendesign-metrics -f docker-compose.metrics.yml \
up -d --force-recreate alertmanager
echo "Alertmanager: контейнер пересоздан — иначе читал бы конфиг по старому иноду."
fi
# ── Caddy: СНАЧАЛА проверить, потом применять ──────────────────
# На этом хосте тот же Caddy обслуживает git., errors. и obsidian.
# Синтаксическая ошибка в infra.caddy положила бы их все, включая

View file

@ -0,0 +1,82 @@
"""Правки конфига Alertmanager обязаны доезжать до работающего контейнера.
ЧТО СЛУЧИЛОСЬ НА ПРОДЕ 27.08. После мержа #3136 на диске лежал уже новый
конфиг `webhook_configs` на сервис кнопки подтверждения, `amtool
check-config` его одобрил, деплой был зелёный. А контейнер продолжал слать
алерты напрямую в Telegram:
на диске: - name: telegram-clients
webhook_configs:
- url: "http://alert-ack:8080/alertmanager"
в контейнере: - name: telegram-clients
telegram_configs:
ПОЧЕМУ. Конфиг подключён бинд-маунтом ФАЙЛА, а рендер в деплое делает `rm` и
создаёт файл заново (иначе не перезаписать: после chown он принадлежит 65534 с
правами 600). `rm` + создание это НОВЫЙ инод, а открытый дескриптор внутри
работающего контейнера продолжает смотреть на прежний, уже удалённый. `up -d`
контейнер не трогает: он сравнивает описание сервиса, а содержимое бинд-маунта
в сравнение не входит.
Отказ беззвучный: ни одного красного признака нигде. Значит и прежние правки
маршрутов применялись лишь тогда, когда контейнер пересоздавался по совпадению.
Перезагрузка по SIGHUP/API не лечит она перечитывает тот же инод. Лечит
только пересоздание контейнера, и именно его наличие здесь и проверяется.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
def _text() -> str:
return WORKFLOW.read_text(encoding="utf-8")
def test_rerendered_config_forces_container_recreate() -> None:
"""После перерисовки конфига контейнер обязан пересоздаваться."""
text = _text()
assert "--force-recreate alertmanager" in text, (
"нет пересоздания Alertmanager — правка конфига не доедет до "
"работающего контейнера, он останется на старом иноде"
)
def test_recreate_is_guarded_by_the_rerender_flag() -> None:
"""Пересоздание — только когда конфиг действительно перерисовали.
Безусловное пересоздание на каждом деплое метрик рвало бы доставку алертов
там, где ничего не менялось.
"""
text = _text()
assert "ALERTMANAGER_RERENDERED=1" in text, "флаг перерисовки не выставляется"
guard = re.search(
r'if \[ "\$\{ALERTMANAGER_RERENDERED:-0\}" = "1" \]; then(.+?)\bfi\b',
text,
re.S,
)
assert guard, "пересоздание не закрыто проверкой флага"
assert "--force-recreate alertmanager" in guard.group(1), (
"пересоздание стоит вне проверки флага — оно будет выполняться всегда"
)
def test_flag_is_set_only_after_config_passed_validation() -> None:
"""Флаг выставляется ПОСЛЕ проверки конфига, а не до неё.
Иначе битый конфиг пересоздавал бы живой контейнер и ронял алертинг целиком
вместо того, чтобы оставить работать прежний.
"""
text = _text()
check = text.index("check-config /tmp/am.yml")
flag = text.index("ALERTMANAGER_RERENDERED=1")
assert check < flag, (
"флаг перерисовки выставлен раньше проверки конфига — битый конфиг "
"убил бы работающий Alertmanager"
)