Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Failing after 1m12s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
Alertmanager инлайн-клавиатуру не поддерживает, а без кнопки нет обратной связи «человек увидел и взял в работу»: 27.08 продукты лежали 10 часов, и вопрос «а кто-нибудь это читает» было не к кому адресовать. ГДЕ ЖИВЁТ. Рядом с Alertmanager, на инфраструктурной машине. У бота МЕРЫ уже есть приём обновлений, и повесить обработку туда было бы дешевле, но он работает на продуктовом хосте: при падении продукта кнопка оказалась бы мёртвой ровно тогда, когда нужна. ССЫЛКА, А НЕ CALLBACK. Callback требует читателя обновлений бота. Бот один, и его обновления уже читает МЕРА — второй читатель получил бы 409 Conflict и отобрал бы сообщения у поддержки. БЕЗ ПАРОЛЯ НА /ack/*, ОСОЗНАННО. Кнопку жмут ночью с телефона, когда лежит прод; требование пароля даст ноль нажатий. Защита — 128-битный токен под конкретное сообщение, живущий сутки; максимум, чего добьётся угадавший, — ложная отметка в чате, где сразу видно, что её поставил не человек. ТОЛЬКО КЛИЕНТСКИЙ МАРШРУТ идёт через сервис. Прочие алерты сохраняют прямой путь в Telegram: чем меньше звеньев, тем надёжнее. Если сервис лёг, Alertmanager повторяет доставку и переуведомляет каждые 30 минут — алерт задерживается, но не теряется. Дублировать вторым прямым каналом не стали: шум в канале тревог опаснее задержки. Девять тестов дёргают настоящие функции, подменяя один шов — вызов Bot API. Важнейший: при отказе отправки с клавиатурой сообщение уходит БЕЗ неё — алерт важнее кнопки.
131 lines
7.5 KiB
Text
131 lines
7.5 KiB
Text
# ═══════════════════════════════════════════════════════════════════════════
|
||
# caddy/sites/infra.caddy — инфраструктурные домены, остающиеся на Beget
|
||
#
|
||
# Переезд GenDesign с Beget (46.173.16.127) на Selectel Poincare
|
||
# (188.124.37.140), окно 30.08.2026. Три домена этого файла ОСТАЮТСЯ на
|
||
# Beget — переезжают только Site Finder/МЕРА-домены (см.
|
||
# caddy/sites/apps.caddy). Общий механизм переключения (переменная
|
||
# CADDY_SITES, `import caddy/sites/{$CADDY_SITES:*}.caddy` в корневом
|
||
# Caddyfile, мотив с ACME rate-limit) описан в шапке apps.caddy — не
|
||
# дублируем здесь, читай там.
|
||
#
|
||
# Домены в этом файле (после переезда — на Beget, CADDY_SITES=infra):
|
||
# - obsidian.gendsgn.ru — Obsidian Self-hosted LiveSync (CouchDB backend)
|
||
# - errors.gendsgn.ru — GlitchTip, self-hosted error tracking
|
||
# - git.gendsgn.ru — Forgejo, self-hosted git + Actions CI
|
||
#
|
||
# На Selectel (CADDY_SITES=apps) этот файл не импортируется вообще — Caddy
|
||
# там даже не видит эти хосты и не пытается выпускать для них сертификаты.
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
# Obsidian Self-hosted LiveSync (CouchDB backend).
|
||
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
||
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
|
||
#
|
||
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
|
||
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
|
||
obsidian.gendsgn.ru {
|
||
encode zstd gzip
|
||
|
||
reverse_proxy couchdb:5984 {
|
||
# Большие документы (vault attachments / images) — увеличиваем timeout
|
||
transport http {
|
||
response_header_timeout 120s
|
||
}
|
||
}
|
||
}
|
||
|
||
# GlitchTip — self-hosted error tracking (Sentry-compatible).
|
||
# DNS: A-record errors.gendsgn.ru → IP VPS.
|
||
errors.gendsgn.ru {
|
||
encode zstd gzip
|
||
|
||
reverse_proxy glitchtip-web:8080
|
||
|
||
log {
|
||
output file /var/log/caddy/errors.gendsgn.ru.log
|
||
}
|
||
}
|
||
|
||
# Forgejo — self-hosted git (migration 2026-05-16).
|
||
# DNS: A-record git.gendsgn.ru → IP VPS.
|
||
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
||
# gendesign_default network. HTTP port 3000 (default Forgejo).
|
||
# Был добавлен вручную при migration, потерян при первом auto-deploy после
|
||
# изменения Caddyfile (deploy.yml делает git reset --hard). См. fix issue.
|
||
git.gendsgn.ru {
|
||
encode zstd gzip
|
||
|
||
reverse_proxy forgejo:3000
|
||
|
||
log {
|
||
output file /var/log/caddy/git.gendsgn.ru.log
|
||
}
|
||
}
|
||
|
||
# Мониторинг — Grafana + приёмник метрик и логов (задача #3078).
|
||
# DNS: A-record metrics.gendsgn.ru → 46.173.16.127 (заведена 2026-08-26).
|
||
#
|
||
# ПОЧЕМУ ЗДЕСЬ, А НЕ В apps.caddy. Наблюдатель сознательно поставлен у ДРУГОГО
|
||
# провайдера, чем наблюдаемое: продукт живёт на Selectel Poincare, и если ляжет
|
||
# он, мониторинг обязан выжить и сказать об этом. Стек поднимается отдельным
|
||
# compose-проектом gendesign-metrics и виден Caddy через сеть gendesign_shared.
|
||
#
|
||
# ДВА КОНТУРА С РАЗНЫМИ УЧЁТКАМИ:
|
||
# /ingest/* — машинный. Агент Alloy с Poincare шлёт сюда remote_write и логи
|
||
# исходящим HTTPS. Благодаря этому на Poincare не открыт НИ ОДИН
|
||
# входящий порт сверх 22/80/443.
|
||
# всё прочее — витрина Grafana под отдельным паролем.
|
||
# Пароль приёмника по построению лежит в открытом виде на продуктовом хосте;
|
||
# разделив учётки, мы не отдаём вместе с ним доступ к дашбордам.
|
||
metrics.gendsgn.ru {
|
||
encode zstd gzip
|
||
|
||
# handle_path срезает префикс: Prometheus получает /api/v1/write,
|
||
# как если бы обращались к нему напрямую.
|
||
handle_path /ingest/prometheus/* {
|
||
import ../metrics-ingest.caddy.snippet
|
||
reverse_proxy prometheus:9090
|
||
}
|
||
|
||
# Loki ожидает путь /loki/api/v1/push — он и остаётся после среза /ingest/loki.
|
||
handle_path /ingest/loki/* {
|
||
import ../metrics-ingest.caddy.snippet
|
||
reverse_proxy loki:3100
|
||
}
|
||
|
||
# Кнопка «Принял в работу» под клиентским инцидентом (#3078).
|
||
#
|
||
# БЕЗ АВТОРИЗАЦИИ НАМЕРЕННО. Кнопку жмут с телефона, ночью, когда лежит
|
||
# прод. Требовать в этот момент ввод пароля — значит получить ноль нажатий
|
||
# и вернуться к вопросу «а кто-нибудь вообще это читает».
|
||
#
|
||
# Защита — неугадываемость и одноразовость: путь содержит 128-битный токен,
|
||
# выданный ровно под это сообщение и живущий сутки. Худшее, чего добьётся
|
||
# угадавший, — ложная отметка «принято» в чате, где сразу видно, что её
|
||
# поставил не человек. Прав в системе токен не даёт никаких.
|
||
#
|
||
# Сервис отвечает только на /ack/* и /healthz; всё прочее — 404.
|
||
handle /ack/* {
|
||
reverse_proxy alert-ack:8080
|
||
}
|
||
|
||
# Вход ОДИН — собственный вход Grafana (#3078). Внешний basic_auth снят по
|
||
# решению владельца: два запроса пароля подряд мешали работе, а Grafana имеет
|
||
# собственную аутентификацию с ролями и `GF_USERS_ALLOW_SIGN_UP=false`.
|
||
#
|
||
# Что при этом теряется, чтобы решение было осознанным: basic_auth отсекал
|
||
# сканеры до Grafana и прикрывал бы её собственную будущую уязвимость. Теперь
|
||
# страница входа Grafana видна из интернета напрямую.
|
||
#
|
||
# Приём метрик НАРОЧНО остаётся под basic_auth (см. handle_path выше): туда
|
||
# ходит агент по паролю, который лежит в открытом виде в окружении
|
||
# продуктового хоста, и отдельная учётка там ограничивает ущерб записью.
|
||
handle {
|
||
reverse_proxy grafana:3000
|
||
}
|
||
|
||
log {
|
||
output file /var/log/caddy/metrics.gendsgn.ru.log
|
||
}
|
||
}
|