Files
jisangs b363f9bcac 운영 DB 백업 스크립트
- 백업용 DB 계정 생성: backup
2026-09-07 18:32:54 +09:00

171 lines
6.5 KiB
Bash

#!/usr/bin/env bash
#
# backup_db.sh
# ------------------------------------------------------------
# chocomae 운영 DB(database 전체) 를 gzip 압축 덤프로 저장하고,
# 4주(28일) 초과 백업 파일은 자동 정리한다.
#
# 실행 위치 : Synology NAS (DSM Task Scheduler)
# 실행 주기 : 매일 1회 (트래픽 낮은 새벽 권장)
#
# 자격증명은 --defaults-extra-file 로 참조하는 옵션 파일에서 읽어옵니다.
# (아래 "1회 초기 설정" 참고)
#
# 1회 초기 설정:
# 1) chmod +x backup_db.sh
# 2) 자격증명 파일 생성 (CREDENTIALS_FILE 경로에):
# [client]
# user=chocomae_backup
# password=여기에_비밀번호
# host=chocomae.jinaju.com
# port=3306
# * 백업 전용 DB 계정을 만들어 그 자격증명만 저장할 것을 권장.
# (필요 권한 예: SELECT, LOCK TABLES, EVENT, TRIGGER, PROCESS, SHOW VIEW)
# 3) chmod 600 <CREDENTIALS_FILE> # 매우 중요 (권한 검사에 걸림)
# 4) DSM > 제어판 > 작업 스케줄러 > 사용자 정의 스크립트로 등록,
# "매일 <원하는 시각>" 스케줄 설정.
#
# 로그: OUTPUT_DIR/backup.log 에 append 됩니다.
# ------------------------------------------------------------
set -euo pipefail
# 배포된 스크립트가 최신인지 로그로 확인할 수 있게 버전 문자열을 남김.
# 코드 변경 시 이 값을 손으로 올린 뒤 배포.
SCRIPT_VERSION="2026-09-07.1"
# 새로 만드는 파일이 세계-읽기가 되지 않도록 umask 를 잠금.
# (Synology 공유 폴더는 상위 디렉터리 ACL 로 777 이 상속되는 경우가 있음)
umask 077
# Synology NAS 의 MariaDB10 패키지 바이너리 경로.
# 배치는 로그인 셸이 아니라 PATH 가 비어 있으므로 명시적으로 추가.
export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin"
# ===================== 설정 =====================
DB_HOST="chocomae.jinaju.com"
DB_PORT="3306"
DB_USER="backup"
DB_NAME="chocomae"
# MySQL 옵션 파일. [client] 섹션에 user/password/host/port 저장.
# 배치가 실행되는 사용자(보통 root) 홈 아래에 두는 것이 자연스럽다.
CREDENTIALS_FILE="/root/.chocomae_db.cnf"
# 백업 파일 저장 위치: 이 스크립트 파일이 있는 디렉터리.
# (호출 위치와 무관하게 항상 스크립트 옆에 저장되도록 절대 경로로 확정)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="$SCRIPT_DIR"
# 보관 기간(일). 이 값을 초과한 파일은 스크립트가 실행될 때 삭제.
RETENTION_DAYS=28
# 로그 파일 크기 제한. 초과 시 마지막 LOG_KEEP_LINES 줄만 남기고 잘라냄.
LOG_MAX_BYTES=$((1 * 1024 * 1024)) # 1 MB
LOG_KEEP_LINES=500
# =================================================
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="${OUTPUT_DIR}/chocomae_${TIMESTAMP}.sql.gz"
LOG_FILE="${OUTPUT_DIR}/backup.log"
mkdir -p "$OUTPUT_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# ---- 사전 점검 ----
if [ ! -f "$CREDENTIALS_FILE" ]; then
log "ERROR: 자격증명 파일이 없습니다: ${CREDENTIALS_FILE}"
exit 1
fi
# 권한이 600 또는 400 이 아니면 실행 중단 (평문 비밀번호 파일 보호)
PERM=$(stat -c '%a' "$CREDENTIALS_FILE" 2>/dev/null || echo "unknown")
if [ "$PERM" != "600" ] && [ "$PERM" != "400" ]; then
log "ERROR: 자격증명 파일 권한이 안전하지 않습니다 (현재: ${PERM}). 'chmod 600 ${CREDENTIALS_FILE}' 실행 필요."
exit 1
fi
# ---- 덤프 명령 탐지 ----
if command -v mariadb-dump >/dev/null 2>&1; then
DUMP_CMD="mariadb-dump"
elif command -v mysqldump >/dev/null 2>&1; then
DUMP_CMD="mysqldump"
else
log "ERROR: mariadb-dump / mysqldump 명령을 찾을 수 없습니다."
exit 1
fi
# ---- 백업 실행 ----
log "backup-db.sh 시작 (v${SCRIPT_VERSION})"
log "백업 시작 : ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
log "출력 파일 : ${OUTPUT_FILE}"
# 옵션 설명:
# --defaults-extra-file : 자격증명 파일 (반드시 다른 옵션보다 먼저 위치)
# --single-transaction : InnoDB 온라인 백업. 락 없이 일관된 스냅샷.
# --routines/--triggers/--events : 스토어드 프로시저/트리거/이벤트 포함.
# --hex-blob : 바이너리 데이터 안전 직렬화.
# --protocol=TCP : NAS→AWS 원격 접속이므로 TCP 강제.
# --default-character-set=utf8mb4 : 한글 안전.
DUMP_ERR=$(mktemp)
if "$DUMP_CMD" \
"--defaults-extra-file=${CREDENTIALS_FILE}" \
--protocol=TCP \
--default-character-set=utf8mb4 \
--single-transaction \
--routines --triggers --events \
--hex-blob \
-h"$DB_HOST" -P"$DB_PORT" \
"$DB_NAME" 2>"$DUMP_ERR" | gzip -c > "$OUTPUT_FILE"; then
# 확장 ACL 이 상속되어 붙어 있을 수 있으므로 명시적으로 제거하고 600 으로 잠금.
if command -v setfacl >/dev/null 2>&1; then
setfacl -b "$OUTPUT_FILE" 2>/dev/null || true
fi
chmod 600 "$OUTPUT_FILE"
SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
log "백업 성공 (크기: ${SIZE})"
else
log "ERROR: 백업 실패 - $(cat "$DUMP_ERR")"
rm -f "$OUTPUT_FILE" "$DUMP_ERR"
exit 1
fi
rm -f "$DUMP_ERR"
# ---- 오래된 백업 정리 ----
log "${RETENTION_DAYS}일 초과 백업 파일 정리 중..."
DELETED_COUNT=0
while IFS= read -r -d '' OLD_FILE; do
log " 삭제: ${OLD_FILE}"
rm -f "$OLD_FILE"
DELETED_COUNT=$((DELETED_COUNT + 1))
done < <(find "$OUTPUT_DIR" -maxdepth 1 -type f \
-name 'chocomae_*.sql.gz' \
-mtime +"${RETENTION_DAYS}" -print0)
log "정리 완료: ${DELETED_COUNT}개 파일 삭제"
# ---- 로그 크기 제한 (rotate) ----
# LOG_MAX_BYTES 초과 시 마지막 LOG_KEEP_LINES 줄만 남기고 축소.
# 이 스크립트만 로그를 쓰는 구조라 tail | mv 방식으로도 레이스가 없음.
if [ -f "$LOG_FILE" ]; then
LOG_SIZE=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$LOG_SIZE" -gt "$LOG_MAX_BYTES" ]; then
tail -n "$LOG_KEEP_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
mv "${LOG_FILE}.tmp" "$LOG_FILE"
log "로그 로테이션: ${LOG_SIZE} 바이트 → 최근 ${LOG_KEEP_LINES}줄로 축소"
fi
fi
log "완료"
# 로그 파일 권한 잠금: 상위 디렉터리 ACL 상속으로 777 이 되는 것을 방지.
# 매 실행 끝에 한 번 정규화하므로 첫 실행/로그 로테이션 모든 경로를 커버함.
if [ -f "$LOG_FILE" ]; then
if command -v setfacl >/dev/null 2>&1; then
setfacl -b "$LOG_FILE" 2>/dev/null || true
fi
chmod 640 "$LOG_FILE"
fi