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
Метрик в проекте не было ни одной: ни экспортеров, ни /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
108 lines
5.3 KiB
Bash
108 lines
5.3 KiB
Bash
#!/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"
|