fix(ops/metrics): правки конфига Alertmanager молча не доезжали до контейнера
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m28s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m28s
Пойман на проде 27.08 сразу после мержа #3136. На диске лежал новый конфиг — `webhook_configs` на сервис кнопки подтверждения, — `amtool check-config` его одобрил, деплой зелёный. А контейнер продолжал слать алерты напрямую: на диске: webhook_configs: url http://alert-ack:8080/alertmanager в контейнере: telegram_configs: Конфиг подключён бинд-маунтом ФАЙЛА, а рендер делает `rm` и создаёт файл заново — иначе не перезаписать: после chown он принадлежит 65534 с правами 600, а каталог принадлежит деплой-пользователю. `rm` + создание даёт НОВЫЙ инод, тогда как открытый дескриптор внутри работающего контейнера продолжает смотреть на прежний, уже удалённый. `up -d` контейнер не трогает: он сравнивает описание сервиса, а содержимое бинд-маунта в сравнение не входит. Отказ беззвучный — ни одного красного признака нигде. Значит и все прежние правки маршрутизации применялись лишь тогда, когда контейнер пересоздавался по совпадению. Перезагрузка по SIGHUP/API не лечит: она перечитывает тот же открытый инод. Лечит только пересоздание контейнера — его и добавляю, под флагом, который выставляется ПОСЛЕ успешной проверки конфига. Порядок важен: при обратном битый конфиг убивал бы работающий Alertmanager вместо того, чтобы оставить прежний работать. Прод уже приведён в соответствие вручную — контейнер пересоздан, маршрут клиентских инцидентов теперь идёт через кнопку. Эта правка нужна, чтобы следующая правка конфига доехала сама. Три теста: пересоздание есть, оно закрыто проверкой флага (безусловное рвало бы доставку на каждом деплое метрик), флаг выставляется после проверки. Прогон: 78 ops-тестов зелёные, ruff чист.
This commit is contained in:
parent
4f77197f01
commit
5ea05cffa6
2 changed files with 111 additions and 0 deletions
|
|
@ -205,6 +205,10 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Алерты: канал задан, конфиг проверен, Alertmanager поднимается."
|
echo "Алерты: канал задан, конфиг проверен, Alertmanager поднимается."
|
||||||
|
# Конфиг перерисован — значит у файла НОВЫЙ инод (см. rm выше).
|
||||||
|
# Помечаем, чтобы ниже пересоздать контейнер: почему это
|
||||||
|
# обязательно — объяснено у самого пересоздания.
|
||||||
|
ALERTMANAGER_RERENDERED=1
|
||||||
else
|
else
|
||||||
echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078."
|
echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078."
|
||||||
fi
|
fi
|
||||||
|
|
@ -219,6 +223,31 @@ jobs:
|
||||||
COMPOSE_PROFILES="$PROFILES" \
|
COMPOSE_PROFILES="$PROFILES" \
|
||||||
docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans
|
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: СНАЧАЛА проверить, потом применять ──────────────────
|
||||||
# На этом хосте тот же Caddy обслуживает git., errors. и obsidian.
|
# На этом хосте тот же Caddy обслуживает git., errors. и obsidian.
|
||||||
# Синтаксическая ошибка в infra.caddy положила бы их все, включая
|
# Синтаксическая ошибка в infra.caddy положила бы их все, включая
|
||||||
|
|
|
||||||
82
backend/tests/ops/test_3xxx_alertmanager_inode.py
Normal file
82
backend/tests/ops/test_3xxx_alertmanager_inode.py
Normal 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"
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue