Стек мониторинга: Prometheus + Alertmanager + Grafana + node_exporter + cAdvisor
Самодостаточный docker compose стек с деплоем через deploy.sh, bootstrap-установкой с нуля и конфигурацией через .env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
74b6f92cb8
15 changed files with 17242 additions and 0 deletions
179
deploy.sh
Normal file
179
deploy.sh
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue