#!/usr/bin/env bash # Идемпотентный деплой стека мониторинга: рендер конфигов из .env, # валидация, запуск, smoke-тесты. Безопасно запускать повторно. set -euo pipefail cd "$(cd "$(dirname "$0")" && pwd)" C_GREEN=$'\033[32m'; C_RED=$'\033[31m'; C_YELLOW=$'\033[33m'; C_RESET=$'\033[0m' ok() { echo "${C_GREEN}[OK]${C_RESET} $*"; } fail() { echo "${C_RED}[FAIL]${C_RESET} $*"; FAILED=1; } warn() { echo "${C_YELLOW}[WARN]${C_RESET} $*"; } die() { echo "${C_RED}[ERR]${C_RESET} $*" >&2; exit 1; } FAILED=0 # --- 1. Проверки окружения -------------------------------------------------- command -v docker >/dev/null 2>&1 || die "docker не найден" docker compose version >/dev/null 2>&1 || die "плагин docker compose (v2) не найден" command -v curl >/dev/null 2>&1 || die "curl не найден (apt install curl)" [ -f .env ] || die "нет .env — выполни: cp .env.example .env && chmod 600 .env, затем заполни" set -a; . ./.env; set +a : "${BIND_IP:?BIND_IP не задан в .env}" : "${SERVER_NAME:?SERVER_NAME не задан в .env}" : "${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD не задан в .env}" METRICS_USER="${METRICS_USER:-admin}" METRICS_PASSWORD="${METRICS_PASSWORD:-}" NODE_EXPORTER_PORT="${NODE_EXPORTER_PORT:-9100}" PROMETHEUS_PORT="${PROMETHEUS_PORT:-9090}" ALERTMANAGER_PORT="${ALERTMANAGER_PORT:-9093}" GRAFANA_PORT="${GRAFANA_PORT:-3001}" CRITICAL_UNITS="${CRITICAL_UNITS:-headscale.service}" CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-}" if ! ip -o addr 2>/dev/null | grep -qF " ${BIND_IP}/"; then warn "адрес ${BIND_IP} не найден на интерфейсах сервера — bind может не подняться" fi # --- 2. Рендер конфигов в rendered/ ------------------------------------------ mkdir -p rendered # 2.1 basic auth: web.yml (bcrypt) + файл пароля для scrape-конфигов Prometheus bcrypt_hash() { if command -v htpasswd >/dev/null 2>&1; then htpasswd -nbB x "$1" | cut -d: -f2 else docker run --rm httpd:2.4-alpine htpasswd -nbB x "$1" | cut -d: -f2 \ || die "не удалось сгенерировать bcrypt-хеш: поставь apache2-utils (htpasswd) или дай docker доступ в сеть" fi } printf '%s' "${METRICS_PASSWORD}" > rendered/password if [ -n "${METRICS_PASSWORD}" ]; then HASH="$(bcrypt_hash "${METRICS_PASSWORD}")" [ -n "${HASH}" ] || die "пустой bcrypt-хеш" printf 'basic_auth_users:\n %s: %s\n' "${METRICS_USER}" "${HASH}" > rendered/web.yml AUTH_ENABLED=1 ok "basic auth включён (пользователь ${METRICS_USER})" else printf '# basic auth отключён: METRICS_PASSWORD пуст\n' > rendered/web.yml AUTH_ENABLED=0 warn "METRICS_PASSWORD пуст — Prometheus/Alertmanager/node_exporter доступны без аутентификации" fi chmod 644 rendered/password rendered/web.yml # 2.2 prometheus.yml из шаблона SED_ARGS=( -e "s|__SERVER_NAME__|${SERVER_NAME}|g" -e "s|__BIND_IP__|${BIND_IP}|g" -e "s|__NODE_EXPORTER_PORT__|${NODE_EXPORTER_PORT}|g" -e "s|__METRICS_USER__|${METRICS_USER}|g" ) if [ "${AUTH_ENABLED}" = 1 ]; then SED_ARGS+=(-e 's|^#AUTH#||') else SED_ARGS+=(-e '/^#AUTH#/d') fi sed "${SED_ARGS[@]}" prometheus/prometheus.yml.tpl > rendered/prometheus.yml ok "rendered/prometheus.yml" # 2.3 правила из .env: SystemdUnitDown + ContainerDown { echo "# Сгенерировано deploy.sh из .env — не редактировать вручную." echo "groups:" echo " - name: generated" echo " rules:" any=0 for u in $(printf '%s' "${CRITICAL_UNITS}" | tr '|,' ' '); do any=1 cat < 120 for: 2m labels: severity: critical container: ${c} annotations: summary: 'Контейнер ${c} не работает' description: 'Контейнер с именем ~ ".*${c}.*" отсутствует или не отвечает дольше 2 минут.' EOF done [ "${any}" = 1 ] || echo " []" } > rendered/generated-rules.yml ok "rendered/generated-rules.yml (юниты: ${CRITICAL_UNITS}; контейнеры: ${CRITICAL_CONTAINERS:-нет})" # --- 3. Валидация конфигурации Prometheus ------------------------------------ echo "— promtool check config…" docker compose run --rm --no-deps --entrypoint promtool prometheus \ check config /etc/prometheus/prometheus.yml \ || die "конфигурация Prometheus не прошла валидацию" # --- 4. Запуск ---------------------------------------------------------------- echo "— docker compose up -d…" docker compose up -d --remove-orphans # перечитать конфиги, если контейнеры уже работали (рендер мог измениться) docker compose kill -s HUP prometheus alertmanager >/dev/null 2>&1 || true # --- 5. Smoke-тесты ----------------------------------------------------------- echo "— smoke-тесты (до 40 секунд на сервис)…" AUTH=() [ -n "${METRICS_PASSWORD}" ] && AUTH=(-u "${METRICS_USER}:${METRICS_PASSWORD}") http_check() { # имя url use_auth(yes|no) local name="$1" url="$2" use_auth="${3:-yes}" i local opts=() [ "${use_auth}" = yes ] && opts=("${AUTH[@]}") for i in $(seq 1 20); do if curl -fsS -o /dev/null --max-time 3 "${opts[@]}" "${url}"; then ok "${name} ${url}" return 0 fi sleep 2 done fail "${name} ${url} — недоступен (docker compose logs ${name})" } http_check node_exporter "http://${BIND_IP}:${NODE_EXPORTER_PORT}/metrics" http_check prometheus "http://${BIND_IP}:${PROMETHEUS_PORT}/-/healthy" http_check alertmanager "http://${BIND_IP}:${ALERTMANAGER_PORT}/-/healthy" http_check grafana "http://${BIND_IP}:${GRAFANA_PORT}/api/health" no cadvisor_ok=0 for i in $(seq 1 10); do if docker compose exec -T cadvisor wget -q -O /dev/null http://localhost:8080/healthz 2>/dev/null; then cadvisor_ok=1; break fi sleep 2 done if [ "${cadvisor_ok}" = 1 ]; then ok "cadvisor внутренний /healthz (наружу не публикуется)" else fail "cadvisor /healthz — недоступен (docker compose logs cadvisor)" fi # --- 6. Итог ------------------------------------------------------------------- echo if [ "${FAILED}" = 0 ]; then echo "Готово. Сервисы:" echo " Grafana: http://${BIND_IP}:${GRAFANA_PORT} (admin / GRAFANA_ADMIN_PASSWORD)" echo " Prometheus: http://${BIND_IP}:${PROMETHEUS_PORT}" echo " Alertmanager: http://${BIND_IP}:${ALERTMANAGER_PORT} (API алертов: /api/v2/alerts)" else echo "Часть проверок провалилась — смотри вывод выше и docker compose logs." exit 1 fi