#!/usr/bin/env bash # PostgreSQL 논리 백업 (pg_dump -Fc). .env의 DB_* 및 PG_BACKUP_* 사용. # PG_BACKUP_SCOPE=all 이면 서버의 연결 가능·비템플릿 DB 전체를 각각 .dump 로 저장. # 운영 서버 cron 예: 0 2 * * * cd /home/xavis/workspace/ai_platform && bash scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1 set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # shellcheck source=scripts/lib/load-env.sh source "$REPO_ROOT/scripts/lib/load-env.sh" load_project_env "$REPO_ROOT/.env" : "${DB_HOST:?DB_HOST is required in .env}" : "${DB_DATABASE:?DB_DATABASE is required in .env}" : "${DB_USERNAME:?DB_USERNAME is required in .env}" : "${DB_PASSWORD:?DB_PASSWORD is required in .env}" DB_PORT="${DB_PORT:-5432}" PG_BACKUP_DIR="${PG_BACKUP_DIR:-/home/xavis/workspace/backup/ai_platform}" PG_BACKUP_RETENTION_DAYS="${PG_BACKUP_RETENTION_DAYS:-30}" PG_BACKUP_SCOPE="${PG_BACKUP_SCOPE:-all}" PG_BACKUP_GLOBALS="${PG_BACKUP_GLOBALS:-1}" export PGHOST="$DB_HOST" export PGPORT="$DB_PORT" if ! command -v pg_dump >/dev/null 2>&1; then echo "pg-backup: pg_dump not found. Install postgresql-client (apt) or postgresql (brew)." >&2 exit 1 fi if ! command -v psql >/dev/null 2>&1; then echo "pg-backup: psql not found. Install postgresql-client." >&2 exit 1 fi log_ts() { date '+%Y-%m-%dT%H:%M:%S%z'; } set_backup_credentials() { if [[ -n "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then export PGUSER="${PG_BACKUP_SUPERUSER:-postgres}" export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD" else export PGUSER="$DB_USERNAME" export PGPASSWORD="$DB_PASSWORD" fi } list_all_databases() { psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 -tAc \ "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY datname;" } dump_database() { local db_name="$1" local dump_file="$2" pg_dump \ -h "$PGHOST" \ -p "$PGPORT" \ -U "$PGUSER" \ -d "$db_name" \ -Fc \ --no-password \ -f "$dump_file" } backup_globals() { if [[ "$PG_BACKUP_GLOBALS" != "1" ]]; then return 0 fi if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then echo "[$(log_ts)] globals skipped: set PG_BACKUP_SUPERUSER_PASSWORD for role/global backup." >&2 return 0 fi local prev_user="$PGUSER" prev_pass="$PGPASSWORD" export PGUSER="${PG_BACKUP_SUPERUSER:-postgres}" export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD" pg_dumpall \ -h "$PGHOST" \ -p "$PGPORT" \ -U "$PGUSER" \ --globals-only \ --no-password \ -f "$RUN_DIR/00_globals.sql" export PGUSER="$prev_user" export PGPASSWORD="$prev_pass" echo "[$(log_ts)] globals saved: $RUN_DIR/00_globals.sql" } set_backup_credentials # cron 일 1회 기준: YYYYMMDD 폴더에 당일 백업 저장 STAMP="$(date +%Y%m%d)" RUN_DIR="$PG_BACKUP_DIR/$STAMP" mkdir -p "$RUN_DIR" echo "[$(log_ts)] pg-backup start → $RUN_DIR (scope=${PG_BACKUP_SCOPE}, retention ${PG_BACKUP_RETENTION_DAYS} days)" if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" && "$PG_BACKUP_SCOPE" == "all" ]]; then echo "[$(log_ts)] note: PG_BACKUP_SUPERUSER_PASSWORD not set; backing up only databases visible to ${DB_USERNAME}." >&2 fi backup_globals declare -a TARGET_DBS=() if [[ "$PG_BACKUP_SCOPE" == "single" ]]; then TARGET_DBS=("$DB_DATABASE") else while IFS= read -r db; do [[ -n "$db" ]] && TARGET_DBS+=("$db") done < <(list_all_databases) if [[ ${#TARGET_DBS[@]} -eq 0 ]]; then echo "pg-backup: no databases found to backup." >&2 exit 1 fi fi MANIFEST="$RUN_DIR/00_manifest.txt" { echo "# pg-backup manifest $(log_ts)" echo "scope=${PG_BACKUP_SCOPE}" echo "host=${PGHOST}:${PGPORT}" echo "user=${PGUSER}" } > "$MANIFEST" dump_ok=0 dump_fail=0 for db_name in "${TARGET_DBS[@]}"; do dump_file="$RUN_DIR/${db_name}.dump" echo "[$(log_ts)] dumping database: $db_name" if dump_database "$db_name" "$dump_file"; then bytes="$(wc -c < "$dump_file" | tr -d ' ')" echo "[$(log_ts)] dump saved: $dump_file (${bytes} bytes)" echo "${db_name}.dump ${bytes}" >> "$MANIFEST" dump_ok=$((dump_ok + 1)) else echo "[$(log_ts)] dump failed: $db_name" >&2 dump_fail=$((dump_fail + 1)) fi done if [[ "$dump_ok" -eq 0 ]]; then echo "pg-backup: all database dumps failed." >&2 exit 1 fi if [[ "$dump_fail" -gt 0 ]]; then echo "[$(log_ts)] warning: ${dump_fail} database dump(s) failed, ${dump_ok} succeeded." >&2 fi ln -sfn "$RUN_DIR" "$PG_BACKUP_DIR/latest" prune_old_backups() { local cutoff removed=0 base day entry if date -d "1 day ago" +%Y%m%d >/dev/null 2>&1; then cutoff=$(date -d "${PG_BACKUP_RETENTION_DAYS} days ago" +%Y%m%d) else cutoff=$(date -v-"${PG_BACKUP_RETENTION_DAYS}"d +%Y%m%d) fi shopt -s nullglob for entry in "$PG_BACKUP_DIR"/*; do [[ -d "$entry" ]] || continue base=$(basename "$entry") [[ "$base" == "latest" ]] && continue if [[ "$base" =~ ^([0-9]{8}) ]]; then day="${BASH_REMATCH[1]}" if [[ "$day" < "$cutoff" ]]; then rm -rf "$entry" removed=$((removed + 1)) echo "[$(log_ts)] retention: removed $entry (backup date $day < cutoff $cutoff)" fi fi done shopt -u nullglob echo "[$(log_ts)] retention: pruned ${removed} dir(s); keeping backups from ${cutoff} onward (${PG_BACKUP_RETENTION_DAYS} days)" } if [[ "$PG_BACKUP_RETENTION_DAYS" =~ ^[0-9]+$ ]] && [[ "$PG_BACKUP_RETENTION_DAYS" -gt 0 ]]; then prune_old_backups fi echo "[$(log_ts)] pg-backup done: ${dump_ok} database(s), latest → $PG_BACKUP_DIR/latest"