From b363f9bcac8992257fd24ce3f959dae93938f981 Mon Sep 17 00:00:00 2001 From: jisangs Date: Mon, 7 Sep 2026 18:32:54 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9A=B4=EC=98=81=20DB=20=EB=B0=B1=EC=97=85=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백업용 DB 계정 생성: backup --- doc/db/260907-daily-db-backup/backup-db.sh | 170 +++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 doc/db/260907-daily-db-backup/backup-db.sh diff --git a/doc/db/260907-daily-db-backup/backup-db.sh b/doc/db/260907-daily-db-backup/backup-db.sh new file mode 100644 index 0000000..cd3665d --- /dev/null +++ b/doc/db/260907-daily-db-backup/backup-db.sh @@ -0,0 +1,170 @@ +#!/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