All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m25s
Джоба agent-apps упала целиком:
error while interpolating services.postgres-exporter-infra.environment.
DATA_SOURCE_NAME: required variable INFRA_EXPORTER_DSN is missing a value
INFRA_EXPORTER_DSN нужен экспортеру с profiles: ["infra"], который на
продуктовом хосте не поднимается вовсе. Но compose интерполирует ВЕСЬ файл
до фильтрации по профилям, поэтому `${VAR:?}` роняет команду из-за чужой
переменной. Вместе с агентом не поднялись alloy, node-exporter и cadvisor,
которым никакой DSN не нужен. Симметрично упал бы и инфраструктурный агент -
на двух продуктовых переменных.
Второй дефект в той же цепочке: GENDESIGN_EXPORTER_DSN тоже отсутствовал.
setup-metrics-exporter-dsn.sh читает только runtime-файл окружения бэкенда, а
DATABASE_URL и TRADEIN_DATABASE_URL живут в основном. Значит подстановка
всегда была пустой, add_key печатал "нечем заполнить" и выходил с кодом 0 -
мягкий пропуск встречался с жёстким требованием compose.
Стало:
- compose: `:-` вместо `:?` у трёх DSN. Интерполяция больше не может упасть.
- deploy-metrics.yml: профиль экспортеров включается, только если нужные ЭТОЙ
роли DSN заполнены; иначе ::warning и агент поднимается без экспортера.
Громкость не убрана, а перенесена туда, где роль известна. Тот же приём, что
уже применён к Alertmanager в джобе server.
- setup-metrics-exporter-dsn.sh: читает оба файла окружения (базовый, затем
runtime - он перекрывает). Пишет по-прежнему только в runtime, лишних копий
пароля не заводит.
Почему `:-` не ослабление: пустой DATA_SOURCE_NAME поднял бы экспортер,
который молча не отдаёт метрик, - ровно тот тихий отказ, ради которого весь
стек и заводится. Поэтому пустой DSN теперь означает "профиль не включаем",
а не "поднимаем пустым".
Известное следствие, отмеченное в коде: INFRA_EXPORTER_DSN не собирает никто -
скрипт знает только про GENDESIGN_/TRADEIN_ и работает на продуктовом хосте.
Пока это так, инфраструктурный агент будет честно предупреждать, что метрик
Postgres инфры нет, вместо того чтобы падать целиком.
Тесты (3) структурные, проверяют оба конца инварианта: обязательности не
вернулись в compose; каждая DSN-переменная проверяется в деплое; профили не
захардкожены. Фальсификация: на исходных файлах краснеют все три.
308 lines
17 KiB
YAML
308 lines
17 KiB
YAML
name: Deploy Metrics
|
||
|
||
# Деплой стека наблюдаемости (#3078). Двухсторонний, и это существенно:
|
||
#
|
||
# server — на ИНФРАСТРУКТУРНЫЙ хост (Beget): Prometheus, Loki, Grafana,
|
||
# Alertmanager. Наблюдатель намеренно живёт у другого провайдера,
|
||
# чем наблюдаемое.
|
||
# agent — на ОБА хоста: node-exporter, cAdvisor, postgres-exporter, Alloy.
|
||
# Агент на продуктовом хосте шлёт push'ем, поэтому там не открывается
|
||
# ни одного входящего порта.
|
||
#
|
||
# Продуктовый стек не трогается вовсе: другой project-name, другие compose-файлы,
|
||
# deploy.yml остаётся в стороне.
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
- "docker-compose.metrics.yml"
|
||
- "docker-compose.metrics-agent.yml"
|
||
- "ops/metrics/**"
|
||
- "caddy/sites/infra.caddy"
|
||
- "caddy/metrics-ui.caddy.snippet"
|
||
- "caddy/metrics-ingest.caddy.snippet"
|
||
# Глоб, а не точечный `setup-metrics-secrets.sh` (#2203: класс бага, а не
|
||
# один файл). Деплой запускает ТРИ setup-скрипта — secrets, grafana-role и
|
||
# exporter-dsn, — а в триггере стоял только первый: правка двух остальных
|
||
# не заводила выкат, и на хосте продолжала исполняться старая версия молча.
|
||
- "scripts/setup-metrics-*.sh"
|
||
- ".forgejo/workflows/deploy-metrics.yml"
|
||
workflow_dispatch:
|
||
|
||
concurrency:
|
||
group: deploy-metrics
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
# ═══ СЕРВЕРНАЯ СТОРОНА — инфраструктурный хост ════════════════════════════
|
||
server:
|
||
runs-on: ubuntu-latest
|
||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
# Тот же приём, что в deploy-obsidian.yml (#3062, #3029): адресат и отпечаток
|
||
# берутся В ПАРЕ, без перекрёстного фолбэка. Сверять ключ Beget'а с отпечатком
|
||
# Poincare — гарантированный отказ.
|
||
- name: Адресат и подлинность инфраструктурного хоста
|
||
id: target
|
||
env:
|
||
INFRA_HOST: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
INFRA_FINGERPRINT: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
MAIN_FINGERPRINT: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${INFRA_HOST:-}" ]; then
|
||
HOST_FINGERPRINT="${INFRA_FINGERPRINT:-}"
|
||
SRC="INFRA_DEPLOY_SSH_FINGERPRINT"
|
||
else
|
||
HOST_FINGERPRINT="${MAIN_FINGERPRINT:-}"
|
||
SRC="DEPLOY_SSH_FINGERPRINT"
|
||
fi
|
||
case "${HOST_FINGERPRINT}" in
|
||
*[![:print:]]*)
|
||
echo "ОШИБКА: ${SRC} содержит перевод строки или непечатный символ." >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
echo "fingerprint=${HOST_FINGERPRINT}" >> "$GITHUB_OUTPUT"
|
||
if [ -z "${HOST_FINGERPRINT:-}" ]; then
|
||
echo "::warning title=SSH без проверки подлинности хоста::${SRC} не задан — ключ хоста НЕ проверяется (#3029)."
|
||
fi
|
||
|
||
- name: Поднять серверный стек
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }}
|
||
username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }}
|
||
key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }}
|
||
port: ${{ secrets.INFRA_DEPLOY_PORT || secrets.DEPLOY_PORT || 22 }}
|
||
fingerprint: ${{ steps.target.outputs.fingerprint }}
|
||
command_timeout: 15m
|
||
script: |
|
||
set -euo pipefail
|
||
cd /opt/gendesign
|
||
|
||
git fetch origin main
|
||
git reset --hard origin/main
|
||
|
||
docker network inspect gendesign_shared >/dev/null 2>&1 \
|
||
|| docker network create gendesign_shared
|
||
|
||
# Окружение нужно в shell, а не только в env_file: проверки вида
|
||
# ${VAR:?} у compose работают по переменным ОКРУЖЕНИЯ ПРОЦЕССА.
|
||
if [ -f backend/.env.runtime ]; then
|
||
set -a; . backend/.env.runtime; set +a
|
||
fi
|
||
|
||
# ── Проверка ДО подъёма, а не после ────────────────────────────
|
||
# Пустой токен даёт Alertmanager, который стартует зелёным и молча
|
||
# ничего не шлёт. Это ровно тот класс тихого отказа, ради которого
|
||
# весь стек и заводится, — ловим на пороге.
|
||
missing=""
|
||
for v in GRAFANA_ADMIN_PASSWORD GLITCHTIP_RO_PASSWORD \
|
||
METRICS_INGEST_USER METRICS_INGEST_PASSWORD; do
|
||
eval "val=\${$v:-}"
|
||
[ -z "$val" ] && missing="$missing $v"
|
||
done
|
||
if [ -n "$missing" ]; then
|
||
echo "ОШИБКА: в окружении хоста не заданы:$missing"
|
||
echo "Запусти один раз: bash scripts/setup-metrics-secrets.sh"
|
||
exit 1
|
||
fi
|
||
|
||
# Алерты включаются, только когда канал доставки реально задан.
|
||
# Поднимать Alertmanager с пустым токеном нельзя: он стартует
|
||
# зелёным и молча ничего не шлёт — ровно тот тихий отказ, ради
|
||
# которого весь стек и заводится.
|
||
PROFILES=""
|
||
if [ -n "${METRICS_TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${METRICS_TELEGRAM_CHAT_ID:-}" ]; then
|
||
PROFILES="alerts"
|
||
mkdir -p ops/metrics/alertmanager
|
||
METRICS_TELEGRAM_BOT_TOKEN="$METRICS_TELEGRAM_BOT_TOKEN" \
|
||
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
||
envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID}' \
|
||
< ops/metrics/alertmanager/alertmanager.yml.tmpl \
|
||
> ops/metrics/alertmanager/alertmanager.yml
|
||
chmod 600 ops/metrics/alertmanager/alertmanager.yml
|
||
echo "Алерты: канал задан, Alertmanager поднимается."
|
||
else
|
||
echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078."
|
||
fi
|
||
|
||
# ── read-only роль для датасорса GlitchTip ─────────────────────
|
||
# Идемпотентно. Прав на запись не выдаём вовсе: датасорс Grafana
|
||
# обязан быть безопасен даже при полном доступе к дашбордам.
|
||
bash scripts/setup-metrics-grafana-role.sh
|
||
|
||
COMPOSE_PROFILES="$PROFILES" \
|
||
docker compose -p gendesign-metrics -f docker-compose.metrics.yml pull --quiet
|
||
COMPOSE_PROFILES="$PROFILES" \
|
||
docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans
|
||
|
||
# ── Caddy: СНАЧАЛА проверить, потом применять ──────────────────
|
||
# На этом хосте тот же Caddy обслуживает git., errors. и obsidian.
|
||
# Синтаксическая ошибка в infra.caddy положила бы их все, включая
|
||
# сам Forgejo, из которого идёт деплой. Поэтому validate — обязателен,
|
||
# и reload делается только после успешной проверки.
|
||
if docker compose -p gendesign -f docker-compose.prod.yml ps caddy --quiet | grep -q .; then
|
||
if docker compose -p gendesign -f docker-compose.prod.yml \
|
||
exec -T caddy caddy validate --config /etc/caddy/Caddyfile; then
|
||
docker compose -p gendesign -f docker-compose.prod.yml \
|
||
exec -T caddy caddy reload --config /etc/caddy/Caddyfile
|
||
echo "Caddy: конфиг проверен и перезагружен."
|
||
else
|
||
echo "ОШИБКА: Caddyfile не проходит проверку — reload НЕ выполнен."
|
||
echo "Работающий Caddy не тронут, домены живы. Чинить конфиг и повторять."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# ── Приёмка ────────────────────────────────────────────────────
|
||
for i in $(seq 1 30); do
|
||
if docker exec gendesign-prometheus wget -q --spider http://localhost:9090/-/healthy 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 3
|
||
done
|
||
docker compose -p gendesign-metrics -f docker-compose.metrics.yml ps
|
||
|
||
# ═══ АГЕНТЫ — оба хоста ═══════════════════════════════════════════════════
|
||
agent-apps:
|
||
runs-on: ubuntu-latest
|
||
needs: server
|
||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Агент на продуктовом хосте
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
host: ${{ secrets.DEPLOY_HOST }}
|
||
username: ${{ secrets.DEPLOY_USER }}
|
||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||
port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||
fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
|
||
command_timeout: 15m
|
||
script: |
|
||
set -euo pipefail
|
||
cd /opt/gendesign
|
||
|
||
git fetch origin main
|
||
git reset --hard origin/main
|
||
|
||
docker network inspect gendesign_shared >/dev/null 2>&1 \
|
||
|| docker network create gendesign_shared
|
||
|
||
if [ -f backend/.env.runtime ]; then
|
||
set -a; . backend/.env.runtime; set +a
|
||
fi
|
||
|
||
if [ -z "${METRICS_INGEST_PASSWORD:-}" ]; then
|
||
echo "ОШИБКА: METRICS_INGEST_PASSWORD не задан — агенту нечем авторизоваться."
|
||
echo "Запусти на инфраструктурном хосте: bash scripts/setup-metrics-secrets.sh"
|
||
exit 1
|
||
fi
|
||
|
||
# DSN экспортеров собираются из уже имеющихся паролей БД, если их
|
||
# ещё нет. Отдельных секретов не заводим — лишняя копия пароля это
|
||
# лишнее место, откуда он может утечь.
|
||
bash scripts/setup-metrics-exporter-dsn.sh
|
||
|
||
set -a; . backend/.env.runtime; set +a
|
||
|
||
# Профиль экспортеров БД включаем, только если DSN реально собрались.
|
||
# Раньше их обязательность стояла в compose (`${VAR:?}`), но compose
|
||
# интерполирует ВЕСЬ файл до фильтрации по профилям — и продуктовый
|
||
# агент падал на INFRA_EXPORTER_DSN, переменной сервиса, который тут
|
||
# не поднимается вовсе. Проверка переехала сюда, где роль известна.
|
||
#
|
||
# Не падаем, а предупреждаем: alloy / node-exporter / cadvisor и сбор
|
||
# логов не должны отваливаться из-за одного ненастроенного экспортера.
|
||
# Тот же приём, что у Alertmanager в джобе server выше.
|
||
EXPORTER_PROFILE=""
|
||
missing_dsn=""
|
||
[ -n "${GENDESIGN_EXPORTER_DSN:-}" ] || missing_dsn="$missing_dsn GENDESIGN_EXPORTER_DSN"
|
||
[ -n "${TRADEIN_EXPORTER_DSN:-}" ] || missing_dsn="$missing_dsn TRADEIN_EXPORTER_DSN"
|
||
if [ -z "$missing_dsn" ]; then
|
||
EXPORTER_PROFILE="apps"
|
||
else
|
||
echo "::warning title=Метрики БД не собираются::не заполнены:$missing_dsn. Хостовые метрики и логи поедут, метрик Postgres не будет. Проверь, что scripts/setup-metrics-exporter-dsn.sh нашёл DATABASE_URL/TRADEIN_DATABASE_URL."
|
||
fi
|
||
|
||
METRICS_ROLE=apps \
|
||
METRICS_ALLOY_CONFIG=alloy-apps.alloy \
|
||
COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml pull --quiet
|
||
|
||
METRICS_ROLE=apps \
|
||
METRICS_ALLOY_CONFIG=alloy-apps.alloy \
|
||
COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml up -d
|
||
|
||
sleep 10
|
||
METRICS_ROLE=apps METRICS_ALLOY_CONFIG=alloy-apps.alloy COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml ps
|
||
|
||
agent-infra:
|
||
runs-on: ubuntu-latest
|
||
needs: server
|
||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Агент на инфраструктурном хосте
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }}
|
||
username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }}
|
||
key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }}
|
||
port: ${{ secrets.INFRA_DEPLOY_PORT || secrets.DEPLOY_PORT || 22 }}
|
||
fingerprint: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
command_timeout: 15m
|
||
script: |
|
||
set -euo pipefail
|
||
cd /opt/gendesign
|
||
|
||
if [ -f backend/.env.runtime ]; then
|
||
set -a; . backend/.env.runtime; set +a
|
||
fi
|
||
|
||
# Симметрично продуктовому агенту: профиль экспортера включаем,
|
||
# только если DSN есть. Без этого гарда экспортер поднялся бы с
|
||
# ПУСТЫМ DATA_SOURCE_NAME (в compose теперь `:-`, а не `:?`) и
|
||
# молча не отдавал бы метрик — ровно тот тихий отказ, ради которого
|
||
# весь стек и заводится.
|
||
#
|
||
# NB: INFRA_EXPORTER_DSN сейчас не собирает никто —
|
||
# scripts/setup-metrics-exporter-dsn.sh знает только про
|
||
# GENDESIGN_/TRADEIN_ и работает на продуктовом хосте. Пока это так,
|
||
# ветка ниже всегда даёт предупреждение, и это честно: метрик
|
||
# инфраструктурной БД действительно нет.
|
||
EXPORTER_PROFILE=""
|
||
if [ -n "${INFRA_EXPORTER_DSN:-}" ]; then
|
||
EXPORTER_PROFILE="infra"
|
||
else
|
||
echo "::warning title=Метрики инфраструктурной БД не собираются::INFRA_EXPORTER_DSN не задан. Хостовые метрики и логи поедут, метрик Postgres инфры не будет."
|
||
fi
|
||
|
||
METRICS_ROLE=infra \
|
||
METRICS_ALLOY_CONFIG=alloy-infra.alloy \
|
||
COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml pull --quiet
|
||
|
||
METRICS_ROLE=infra \
|
||
METRICS_ALLOY_CONFIG=alloy-infra.alloy \
|
||
COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml up -d
|
||
|
||
sleep 10
|
||
METRICS_ROLE=infra METRICS_ALLOY_CONFIG=alloy-infra.alloy COMPOSE_PROFILES="$EXPORTER_PROFILE" \
|
||
docker compose -p gendesign-metrics-agent \
|
||
-f docker-compose.metrics-agent.yml ps
|