feat(ops): pg-backup.sh — PostgreSQL 每 6h 自動備份

備份目標 (188):
- awoooi_prod (host PostgreSQL, TCP 127.0.0.1)
- momo_analytics (momo-db 容器)

功能:
- gzip 壓縮,保留 7 天自動清理
- Telegram 通知 (成功/失敗)
- cron 0 */6 * * * 已設定

驗證: 兩個 DB 備份成功 (awoooi_prod 206K, gz 完整)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 09:16:21 +08:00
parent 9af281cc98
commit f98be41517

117
scripts/ops/pg-backup.sh Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# scripts/ops/pg-backup.sh
# Sprint 5.2: PostgreSQL 自動備份腳本
# 部署: cron 0 */6 * * * on 188 (ollama user)
# 備份目標: awoooi_prod + momo_analytics
# 保留策略: 7 天
# 2026-04-09 Claude Sonnet 4.6 Asia/Taipei
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/home/ollama/backups}"
SECRETS_FILE="${SECRETS_FILE:-/home/ollama/awoooi-ops-secrets/secrets.env}"
RETAIN_DAYS="${RETAIN_DAYS:-7}"
AWOOOI_API_URL="${AWOOOI_API_URL:-https://awoooi.wooo.work}"
# 載入 secrets含 Telegram token for fallback
[[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE"
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S %z')]"
log() { echo "${LOG_PREFIX} $*"; }
notify_telegram() {
local msg="$1"
if [[ -n "${TELEGRAM_BOT_TOKEN:-}" && -n "${TELEGRAM_CHAT_ID:-}" ]]; then
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\":\"${TELEGRAM_CHAT_ID}\",\"text\":\"${msg}\",\"parse_mode\":\"HTML\"}" \
> /dev/null 2>&1 || true
fi
}
backup_db() {
local label="$1" # awoooi_prod | momo_analytics
local host="$2" # 127.0.0.1
local user="$3"
local password="$4"
local dbname="$5"
local outfile="${BACKUP_DIR}/${label}_${TIMESTAMP}.sql.gz"
log "開始備份 ${label}${outfile}"
if PGPASSWORD="$password" pg_dump \
-h "$host" -U "$user" -d "$dbname" \
--no-owner --no-acl \
2>/dev/null | gzip > "$outfile"; then
local size
size=$(du -sh "$outfile" | cut -f1)
log "${label} 備份完成 (${size})"
echo "success:${label}:${size}"
else
log "${label} 備份失敗"
echo "failed:${label}"
fi
}
cleanup_old_backups() {
local label="$1"
local count
count=$(find "$BACKUP_DIR" -name "${label}_*.sql.gz" -mtime "+${RETAIN_DAYS}" | wc -l)
if (( count > 0 )); then
find "$BACKUP_DIR" -name "${label}_*.sql.gz" -mtime "+${RETAIN_DAYS}" -delete
log "🗑️ 清理 ${label} 舊備份 ${count} 個 (>${RETAIN_DAYS}天)"
fi
}
main() {
mkdir -p "$BACKUP_DIR"
log "=== pg-backup 開始 (retain=${RETAIN_DAYS}d) ==="
local results=()
# awoooi_prod (host PostgreSQL, TCP)
results+=("$(backup_db "awoooi_prod" "127.0.0.1" "awoooi" "awoooi_prod_2026" "awoooi_prod")")
# momo_analytics (momo-db 容器,透過 host TCP)
local momo_pass
momo_pass=$(docker inspect momo-db --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep POSTGRES_PASSWORD | cut -d= -f2)
if [[ -n "$momo_pass" ]]; then
results+=("$(backup_db "momo_analytics" "127.0.0.1" "momo" "$momo_pass" "momo_analytics")")
else
log "⚠️ momo-db 密碼未取得,跳過 momo_analytics"
results+=("skipped:momo_analytics")
fi
# 清理舊備份
cleanup_old_backups "awoooi_prod"
cleanup_old_backups "momo_analytics"
log "=== pg-backup 完成 ==="
# 組裝 Telegram 通知
local success_count=0 fail_count=0 details=""
for r in "${results[@]}"; do
IFS=':' read -r status label size_or_empty <<< "$r"
case "$status" in
success) ((success_count++)) || true; details+="${label} (${size_or_empty})\n" ;;
failed) ((fail_count++)) || true; details+="${label} 失敗\n" ;;
skipped) details+="⏭️ ${label} 跳過\n" ;;
esac
done
local icon="✅"
[[ $fail_count -gt 0 ]] && icon="⚠️"
notify_telegram "${icon} <b>AWOOOI DB 備份</b>
├ 時間: $(date '+%Y-%m-%d %H:%M') +0800
├ 成功: ${success_count} | 失敗: ${fail_count}
${details}"
[[ $fail_count -gt 0 ]] && exit 1
return 0
}
main "$@"