#!/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 # 매우 중요 (권한 검사에 걸림) # 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