gendesign/scripts/setup-metrics-secrets.sh
bot-backend 309d273f3f
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(observability): стек метрик и логов — Prometheus, Loki, Grafana, агенты на обоих хостах
Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах,
единственный канал наблюдения — journald, единственный сигнал об аварии —
исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе:
задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь
месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль
newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992).

Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает
Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ.
Здесь появляется другой класс данных: ряды и алерты по трендам.

Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона
на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом
сказать, а не лечь вместе с ним.

Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим
HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443.
При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации
терял бы точки именно в аварии, ради которой мониторинг и нужен.

Два контура доступа с разными учётками. Пароль приёмника по построению лежит
открытым на продуктовом хосте, значит его компрометация неизбежна вместе с
хостом; будь это учётка витрины, утёк бы и доступ к дашбордам.

GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6
stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в
grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена
на живой базе: колонка времени называется timestamp, а не received, и отдельной
таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue.

Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не
должен на нём стоять. Деплой предупреждает, что уведомлять пока некому.

Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus
задана и по времени и по размеру, retention_enabled у компактора Loki (без него
retention_period не работает вовсе), путь к журналу и запуск Alloy от root
(иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки
(на этом хосте тот же Caddy держит git, errors и obsidian).

Refs #3078
2026-08-26 10:45:54 +03:00

108 lines
5.3 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Разовая подготовка учётных данных стека наблюдаемости (#3078).
#
# Запускать НА ИНФРАСТРУКТУРНОМ ХОСТЕ (Beget), один раз. Идемпотентен: уже
# заданные значения переиспользуются, ничего не перезаписывается.
#
# ЧТО ДЕЛАЕТ:
# 1. Генерирует пароли приёмника, витрины, администратора Grafana и read-only
# роли к базе GlitchTip; дописывает их в окружение хоста.
# 2. Кладёт пароль приёмника на продуктовый хост — агенту нужно им
# авторизоваться при отправке метрик и логов.
# 3. Печатает bcrypt-хеши для caddy/metrics-*.caddy.snippet.
#
# ЧЕГО НЕ ДЕЛАЕТ: не печатает сами пароли. Хеш публиковать безопасно, пароль —
# нет, а вывод скрипта попадает в журнал деплоя и в историю терминала.
#
# Токен Telegram скрипт НЕ генерирует — его нужно задать руками:
# METRICS_TELEGRAM_BOT_TOKEN, METRICS_TELEGRAM_CHAT_ID
set -euo pipefail
ENVF=/opt/gendesign/backend/.env.runtime
REMOTE_ENVF=/opt/gendesign/backend/.env.runtime
RSSH=(ssh -o BatchMode=yes -o ConnectTimeout=15 selectel)
STAMP=$(date -u +%Y%m%d%H%M%S)
if [ ! -f "$ENVF" ]; then
echo "ОШИБКА: не найден файл окружения бэкенда на этом хосте." >&2
exit 1
fi
# Копия перед первой правкой — одна на запуск, а не на каждый ключ.
backup_once() {
[ -f "${ENVF}.bak-metrics-${STAMP}" ] && return 0
cp -p "$ENVF" "${ENVF}.bak-metrics-${STAMP}"
}
have() { grep -qE "^${1}=" "$ENVF" 2>/dev/null; }
value_of() { grep -m1 -E "^${1}=" "$ENVF" | cut -d= -f2-; }
# 32 символа из [A-Za-z0-9]: помещается в basic_auth без экранирования и не
# ломает разбор .env, где кавычки и знак равенства создают сюрпризы.
gen_pass() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32; }
ensure() {
local key="$1" val="$2"
if have "$key"; then
echo " $key — уже есть, переиспользую"
else
backup_once
printf '%s=%s\n' "$key" "$val" >> "$ENVF"
echo " $key — сгенерирован"
fi
}
echo "=== учётки на инфраструктурном хосте ==="
ensure METRICS_INGEST_USER "alloy"
ensure METRICS_INGEST_PASSWORD "$(gen_pass)"
ensure METRICS_UI_USER "metrics"
ensure METRICS_UI_PASSWORD "$(gen_pass)"
ensure GRAFANA_ADMIN_USER "admin"
ensure GRAFANA_ADMIN_PASSWORD "$(gen_pass)"
ensure GLITCHTIP_RO_PASSWORD "$(gen_pass)"
INGEST_PASS=$(value_of METRICS_INGEST_PASSWORD)
UI_PASS=$(value_of METRICS_UI_PASSWORD)
GT_RO=$(value_of GLITCHTIP_RO_PASSWORD)
ensure INFRA_EXPORTER_DSN \
"postgresql://grafana_ro:${GT_RO}@gendesign-infra-postgres:5432/glitchtip?sslmode=disable"
echo
echo "=== пароль приёмника на продуктовый хост ==="
if "${RSSH[@]}" "grep -qE '^METRICS_INGEST_PASSWORD=' ${REMOTE_ENVF}" 2>/dev/null; then
echo " уже задан, не трогаю"
else
"${RSSH[@]}" "cp -p ${REMOTE_ENVF} ${REMOTE_ENVF}.bak-metrics-${STAMP}"
# Значение идёт по stdin, а не аргументом команды: аргументы видны в
# `ps` на обеих машинах и оседают в истории оболочки.
printf 'METRICS_INGEST_USER=alloy\nMETRICS_INGEST_PASSWORD=%s\n' "$INGEST_PASS" \
| "${RSSH[@]}" "cat >> ${REMOTE_ENVF}"
"${RSSH[@]}" "chown --reference=${REMOTE_ENVF}.bak-metrics-${STAMP} ${REMOTE_ENVF}; \
chmod --reference=${REMOTE_ENVF}.bak-metrics-${STAMP} ${REMOTE_ENVF}"
echo " записан, права сохранены по резервной копии"
fi
echo
echo "=== bcrypt-хеши для caddy/metrics-*.caddy.snippet ==="
echo " (пароли не печатаются; хеши безопасны для git)"
echo
for pair in "alloy:${INGEST_PASS}:metrics-ingest" "metrics:${UI_PASS}:metrics-ui"; do
user="${pair%%:*}"; rest="${pair#*:}"
pass="${rest%%:*}"; file="${rest#*:}"
hash=$(docker run --rm caddy:2 caddy hash-password --plaintext "$pass" 2>/dev/null | tr -d '\r\n')
b64=$(printf '%s' "$hash" | base64 -w 0)
printf ' caddy/%s.caddy.snippet\n %-8s %s\n' "$file" "$user" "$b64"
done
echo
echo "=== что осталось сделать руками ==="
have METRICS_TELEGRAM_BOT_TOKEN \
&& echo " METRICS_TELEGRAM_BOT_TOKEN — задан" \
|| echo " METRICS_TELEGRAM_BOT_TOKEN — НЕ задан, алерты никуда не уйдут"
have METRICS_TELEGRAM_CHAT_ID \
&& echo " METRICS_TELEGRAM_CHAT_ID — задан" \
|| echo " METRICS_TELEGRAM_CHAT_ID — НЕ задан, алерты никуда не уйдут"
echo
echo " Пароль витрины смотреть так (в переписку не копировать):"
echo " grep '^METRICS_UI_PASSWORD=' $ENVF"