Самодостаточный docker compose стек с деплоем через deploy.sh, bootstrap-установкой с нуля и конфигурацией через .env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
7.6 KiB
Bash
179 lines
7.6 KiB
Bash
#!/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
|