server-monitoring/deploy.sh
ShiZa039 45376bfa8d Исполняемые биты для deploy.sh, bootstrap.sh, entrypoint.sh
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:54:53 +02:00

179 lines
7.6 KiB
Bash
Executable file
Raw Permalink 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
# Идемпотентный деплой стека мониторинга: рендер конфигов из .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 <<EOF
- alert: SystemdUnitDown
expr: node_systemd_unit_state{name="${u}",state="active"} == 0 or absent(node_systemd_unit_state{name="${u}"})
for: 1m
labels:
severity: critical
unit: ${u}
annotations:
summary: 'systemd-юнит ${u} не активен'
description: 'Юнит ${u} не в состоянии active (или метрика недоступна) дольше 1 минуты.'
EOF
done
for c in $(printf '%s' "${CRITICAL_CONTAINERS}" | tr '|,' ' '); do
any=1
cat <<EOF
- alert: ContainerDown
expr: absent(container_last_seen{name=~".*${c}.*"}) or (time() - max(container_last_seen{name=~".*${c}.*"})) > 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