- prod-backup.sh (tier hourly 48ч / daily 30д + uploads), prod-backup-offsite-pull.sh (DR на dev-vm) - prod-restore-verify.sh: restore во временную БД + сверка числа строк, дроп - prod-monitor.sh: api/web/public/БД/диск/5xx → Telegram владельцу (+ --test) - deploy-prod.sh: guard'ы (prod-postgres, .env chmod600, не dev-пароль, quick-login off, SEED_DEMO_DATA=false, issuer), бэкап ДО миграций, только --yes, НЕ в ночных - systemd таймеры (backup-hourly/daily, monitor, offsite-pull); docs/prod-runbook.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
5.3 KiB
Bash
Executable file
87 lines
5.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# ПРОД food-market — деплой ТОЛЬКО по явной команде (--yes). Делает БЭКАП ДО миграций.
|
||
# НЕ встраивать в ночные задачи: прод обновляется вручную.
|
||
#
|
||
# Запуск (с dev-vm):
|
||
# ~/deploy-prod.sh <api-tag> <web-tag> --yes # сборка+push+деплой
|
||
# ~/deploy-prod.sh --yes # взять теги из прод .env (не пересобирать)
|
||
#
|
||
# Guards: прод-compose указывает на food-market-prod-postgres (не shared-postgres),
|
||
# .env chmod 600, quick-login выкл, демо-данные выкл, issuer = admin.food-market.kz.
|
||
# Публичный сайт (public) НЕ трогается.
|
||
set -uo pipefail
|
||
|
||
PROD_HOST="${PROD_HOST:-nns@192.168.1.190}"
|
||
PROD_DIR="${PROD_DIR:-~/food-market/deploy}"
|
||
REGISTRY="${REGISTRY:-192.168.1.193:5001}"
|
||
REPO="${REPO:-/home/nns/food-market}"
|
||
|
||
API_TAG=""; WEB_TAG=""; YES=0; BUILD=0
|
||
for a in "$@"; do
|
||
case "$a" in
|
||
--yes) YES=1 ;;
|
||
--build) BUILD=1 ;;
|
||
-*) echo "Unknown flag: $a" >&2; exit 2 ;;
|
||
*) if [[ -z "$API_TAG" ]]; then API_TAG="$a"; elif [[ -z "$WEB_TAG" ]]; then WEB_TAG="$a"; fi ;;
|
||
esac
|
||
done
|
||
|
||
if [[ $YES -ne 1 ]]; then
|
||
echo "ОТКАЗ: прод-деплой требует явного --yes. Это боевой сервер."
|
||
echo "Usage: $0 [<api-tag> <web-tag>] --yes [--build]"
|
||
exit 2
|
||
fi
|
||
|
||
log() { echo "[$(date -Is)] [deploy-prod] $*"; }
|
||
fail() { echo "[deploy-prod] FAIL: $*" >&2; exit 1; }
|
||
|
||
# ── Guard 1: прод-compose не ссылается на чужой shared-postgres ──
|
||
REMOTE_COMPOSE="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cat $PROD_DIR/docker-compose.yml" 2>/dev/null)" || fail "не прочитать прод-compose"
|
||
echo "$REMOTE_COMPOSE" | grep -q 'container_name: food-market-prod-postgres' || fail "в прод-compose нет food-market-prod-postgres"
|
||
if echo "$REMOTE_COMPOSE" | grep -qE 'container_name:[[:space:]]*shared-postgres|name:[[:space:]]*food-market-postgres-data[[:space:]]*$|127\.0\.0\.1:5434:5432'; then
|
||
fail "прод-compose ссылается на shared-postgres/чужой том/порт 5434 — СТОП"
|
||
fi
|
||
|
||
# ── Guard 2: прод .env санитария ──
|
||
REMOTE_ENV="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cat $PROD_DIR/.env" 2>/dev/null)" || fail "не прочитать прод .env"
|
||
PERM="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "stat -c %a $PROD_DIR/.env" 2>/dev/null)"
|
||
[[ "$PERM" == "600" ]] || fail "прод .env права $PERM (должно 600)"
|
||
echo "$REMOTE_ENV" | grep -qiE '^POSTGRES_PASSWORD=food_market_dev$' && fail "прод использует dev-пароль БД — СТОП"
|
||
echo "$REMOTE_ENV" | grep -qiE '^QUICK_LOGIN_ENABLED=true' && fail "на проде включён quick-login — СТОП"
|
||
echo "$REMOTE_ENV" | grep -qiE '^SEED_DEMO_DATA=false' || fail "на проде не выставлен SEED_DEMO_DATA=false — СТОП"
|
||
echo "$REMOTE_ENV" | grep -qiE '^OPENIDDICT_ISSUER=https://admin\.food-market\.kz' || fail "issuer не admin.food-market.kz — СТОП"
|
||
log "guards OK"
|
||
|
||
# ── (опц.) сборка + push релизных образов ──
|
||
if [[ $BUILD -eq 1 ]]; then
|
||
[[ -n "$API_TAG" && -n "$WEB_TAG" ]] || { API_TAG="$(cd "$REPO" && git rev-parse --short HEAD)"; WEB_TAG="$API_TAG"; }
|
||
log "build+push api=$API_TAG web=$WEB_TAG"
|
||
( cd "$REPO" && docker build --build-arg LOCAL_REGISTRY=$REGISTRY --build-arg GIT_SHA="$API_TAG" -f deploy/Dockerfile.api -t $REGISTRY/food-market-api:$API_TAG . ) || fail "build api"
|
||
( cd "$REPO" && docker build --build-arg LOCAL_REGISTRY=$REGISTRY -f deploy/Dockerfile.web -t $REGISTRY/food-market-web:$WEB_TAG . ) || fail "build web"
|
||
docker push $REGISTRY/food-market-api:$API_TAG || fail "push api"
|
||
docker push $REGISTRY/food-market-web:$WEB_TAG || fail "push web"
|
||
# обновить теги в прод .env
|
||
ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cd $PROD_DIR && sed -i 's/^API_TAG=.*/API_TAG=$API_TAG/; s/^WEB_TAG=.*/WEB_TAG=$WEB_TAG/' .env"
|
||
fi
|
||
|
||
# ── БЭКАП ДО миграций (обязательно) ──
|
||
log "бэкап БД ДО деплоя…"
|
||
ssh -o StrictHostKeyChecking=no "$PROD_HOST" "FM_BACKUP_TIER=daily $PROD_DIR/prod-backup.sh" 2>&1 | tail -4 || fail "бэкап не прошёл — деплой остановлен"
|
||
|
||
# ── деплой: pull + up postgres+api+web (НЕ public) ──
|
||
log "pull + up (миграции применяются при старте api)…"
|
||
ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cd $PROD_DIR && docker compose pull postgres api web && docker compose up -d postgres api web" 2>&1 | tail -10 || fail "compose up"
|
||
|
||
# ── ждём healthy + smoke ──
|
||
log "жду api healthy…"
|
||
OK=0
|
||
for i in $(seq 1 30); do
|
||
if ssh -o StrictHostKeyChecking=no "$PROD_HOST" "curl -fsS http://localhost:8080/health/ready 2>/dev/null | grep -q Healthy"; then OK=1; break; fi
|
||
sleep 3
|
||
done
|
||
[[ $OK -eq 1 ]] || fail "api не стал Healthy — проверь логи food-market-api"
|
||
PCODE=$(curl -s -o /dev/null -w '%{http_code}' https://admin.food-market.kz/health/ready 2>/dev/null || echo 000)
|
||
log "публичный /health/ready → HTTP $PCODE"
|
||
log "✓ прод-деплой завершён (api=$(ssh $PROD_HOST "grep ^API_TAG= $PROD_DIR/.env | cut -d= -f2"))"
|