Backup
백업
데이터 손실에 대비하여 복사본을 생성하는 작업입니다. 풀 백업, 증분 백업, 차등 백업으로 구분되며, 재해 복구(Disaster Recovery)의 핵심 기반이 됩니다.
백업
데이터 손실에 대비하여 복사본을 생성하는 작업입니다. 풀 백업, 증분 백업, 차등 백업으로 구분되며, 재해 복구(Disaster Recovery)의 핵심 기반이 됩니다.
풀 백업(Full Backup)은 모든 데이터를 완전히 복사하는 방식입니다. 복원이 가장 간단하지만, 시간과 저장 공간이 많이 필요합니다. 주로 주 단위 또는 월 단위로 수행합니다.
증분 백업(Incremental Backup)은 마지막 백업 이후 변경된 데이터만 복사합니다. 백업 시간과 공간이 적게 들지만, 복원 시 풀 백업 + 모든 증분 백업을 순차적으로 적용해야 합니다. PostgreSQL의 WAL 아카이빙이 대표적인 예입니다.
차등 백업(Differential Backup)은 마지막 풀 백업 이후 변경된 모든 데이터를 복사합니다. 증분 백업보다 크지만, 복원 시 풀 백업 + 가장 최근 차등 백업만 있으면 됩니다.
논리적 백업 vs 물리적 백업: 논리적 백업(pg_dump, mysqldump)은 SQL 형태로 데이터를 추출하여 이식성이 좋지만 느립니다. 물리적 백업(pg_basebackup, Percona XtraBackup)은 데이터 파일을 직접 복사하여 빠르지만 동일 버전에서만 복원 가능합니다.
3-2-1 백업 규칙은 업계 표준 모범 사례입니다: 최소 3개의 데이터 복사본, 2개의 다른 저장 매체, 1개는 오프사이트(클라우드, 원격지)에 보관해야 합니다.
# 데이터베이스 백업 자동화 - Python
import subprocess
import boto3
from datetime import datetime
from pathlib import Path
import gzip
import os
class DatabaseBackup:
"""PostgreSQL/MySQL 백업 관리 클래스"""
def __init__(self, db_config: dict, s3_bucket: str = None):
self.db_config = db_config
self.s3_bucket = s3_bucket
self.backup_dir = Path("/var/backups/db")
self.backup_dir.mkdir(parents=True, exist_ok=True)
def create_postgresql_backup(self, backup_type: str = "full") -> Path:
"""
PostgreSQL 백업 생성
- full: pg_dump로 전체 백업
- basebackup: pg_basebackup으로 물리적 백업
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
db_name = self.db_config['database']
if backup_type == "full":
# 논리적 백업 (SQL 덤프)
backup_file = self.backup_dir / f"{db_name}_{timestamp}.sql.gz"
# pg_dump 실행 후 gzip 압축
dump_cmd = [
"pg_dump",
"-h", self.db_config['host'],
"-p", str(self.db_config.get('port', 5432)),
"-U", self.db_config['user'],
"-d", db_name,
"-F", "p", # plain SQL format
"--no-owner",
"--no-acl"
]
env = os.environ.copy()
env['PGPASSWORD'] = self.db_config['password']
with gzip.open(backup_file, 'wt') as f:
result = subprocess.run(
dump_cmd, stdout=f, stderr=subprocess.PIPE,
env=env, text=True
)
if result.returncode != 0:
raise Exception(f"Backup failed: {result.stderr}")
print(f"✅ Full backup created: {backup_file}")
elif backup_type == "basebackup":
# 물리적 백업 (PITR 지원)
backup_file = self.backup_dir / f"{db_name}_base_{timestamp}"
basebackup_cmd = [
"pg_basebackup",
"-h", self.db_config['host'],
"-p", str(self.db_config.get('port', 5432)),
"-U", self.db_config['user'],
"-D", str(backup_file),
"-Ft", # tar format
"-z", # gzip compression
"-P" # progress
]
env = os.environ.copy()
env['PGPASSWORD'] = self.db_config['password']
result = subprocess.run(basebackup_cmd, env=env)
if result.returncode != 0:
raise Exception("Base backup failed")
print(f"✅ Base backup created: {backup_file}")
return backup_file
def upload_to_s3(self, backup_file: Path, retention_days: int = 30):
"""백업 파일을 S3에 업로드 (3-2-1 규칙의 오프사이트 보관)"""
if not self.s3_bucket:
raise ValueError("S3 bucket not configured")
s3 = boto3.client('s3')
s3_key = f"backups/{backup_file.name}"
# 업로드
s3.upload_file(str(backup_file), self.s3_bucket, s3_key)
# 수명 주기 태그 설정
s3.put_object_tagging(
Bucket=self.s3_bucket,
Key=s3_key,
Tagging={'TagSet': [
{'Key': 'retention-days', 'Value': str(retention_days)}
]}
)
print(f"✅ Uploaded to S3: s3://{self.s3_bucket}/{s3_key}")
def cleanup_old_backups(self, retention_days: int = 7):
"""오래된 로컬 백업 삭제"""
import time
cutoff_time = time.time() - (retention_days * 86400)
for backup_file in self.backup_dir.glob("*.sql.gz"):
if backup_file.stat().st_mtime < cutoff_time:
backup_file.unlink()
print(f"🗑️ Deleted old backup: {backup_file.name}")
# 사용 예시
if __name__ == "__main__":
config = {
'host': 'localhost',
'database': 'myapp',
'user': 'backup_user',
'password': 'secure_password'
}
backup = DatabaseBackup(config, s3_bucket="my-db-backups")
# 일일 풀 백업
backup_file = backup.create_postgresql_backup("full")
# S3에 업로드 (오프사이트 보관)
backup.upload_to_s3(backup_file, retention_days=30)
# 7일 이상 된 로컬 백업 삭제
backup.cleanup_old_backups(retention_days=7)
// 데이터베이스 백업 및 S3 업로드 - Node.js
const { spawn } = require('child_process');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { createReadStream, createWriteStream, unlinkSync } = require('fs');
const { createGzip } = require('zlib');
const { pipeline } = require('stream/promises');
const path = require('path');
class DatabaseBackup {
constructor(dbConfig, s3Config) {
this.dbConfig = dbConfig;
this.s3Client = new S3Client({ region: s3Config.region });
this.s3Bucket = s3Config.bucket;
this.backupDir = '/var/backups/db';
}
/**
* PostgreSQL 백업 생성
*/
async createPostgresBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${this.dbConfig.database}_${timestamp}.sql`;
const backupPath = path.join(this.backupDir, filename);
const compressedPath = `${backupPath}.gz`;
return new Promise((resolve, reject) => {
const env = { ...process.env, PGPASSWORD: this.dbConfig.password };
const pgDump = spawn('pg_dump', [
'-h', this.dbConfig.host,
'-p', this.dbConfig.port || '5432',
'-U', this.dbConfig.user,
'-d', this.dbConfig.database,
'-F', 'p',
'--no-owner'
], { env });
const writeStream = createWriteStream(backupPath);
pgDump.stdout.pipe(writeStream);
pgDump.stderr.on('data', (data) => {
console.error(`pg_dump stderr: ${data}`);
});
pgDump.on('close', async (code) => {
if (code !== 0) {
reject(new Error(`pg_dump exited with code ${code}`));
return;
}
// Gzip 압축
await pipeline(
createReadStream(backupPath),
createGzip(),
createWriteStream(compressedPath)
);
// 원본 삭제
unlinkSync(backupPath);
console.log(`✅ Backup created: ${compressedPath}`);
resolve(compressedPath);
});
});
}
/**
* MySQL 백업 생성
*/
async createMysqlBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${this.dbConfig.database}_${timestamp}.sql.gz`;
const backupPath = path.join(this.backupDir, filename);
return new Promise((resolve, reject) => {
const mysqldump = spawn('mysqldump', [
'-h', this.dbConfig.host,
'-P', this.dbConfig.port || '3306',
'-u', this.dbConfig.user,
`-p${this.dbConfig.password}`,
'--single-transaction', // InnoDB 일관성 보장
'--routines', // 스토어드 프로시저 포함
'--triggers', // 트리거 포함
this.dbConfig.database
]);
const gzip = createGzip();
const writeStream = createWriteStream(backupPath);
mysqldump.stdout.pipe(gzip).pipe(writeStream);
mysqldump.on('close', (code) => {
if (code === 0) {
console.log(`✅ MySQL backup created: ${backupPath}`);
resolve(backupPath);
} else {
reject(new Error(`mysqldump exited with code ${code}`));
}
});
});
}
/**
* S3에 백업 업로드
*/
async uploadToS3(backupPath) {
const filename = path.basename(backupPath);
const s3Key = `backups/${filename}`;
const command = new PutObjectCommand({
Bucket: this.s3Bucket,
Key: s3Key,
Body: createReadStream(backupPath),
ContentType: 'application/gzip',
Tagging: 'backup-type=automated&retention=30days'
});
await this.s3Client.send(command);
console.log(`✅ Uploaded to S3: s3://${this.s3Bucket}/${s3Key}`);
return `s3://${this.s3Bucket}/${s3Key}`;
}
/**
* 전체 백업 워크플로우
*/
async runBackupWorkflow() {
try {
console.log('🚀 Starting backup workflow...');
// 1. 백업 생성
const backupPath = await this.createPostgresBackup();
// 2. S3 업로드 (오프사이트 보관)
const s3Uri = await this.uploadToS3(backupPath);
// 3. 알림 전송 (Slack, Email 등)
console.log(`✅ Backup complete: ${s3Uri}`);
return { success: true, location: s3Uri };
} catch (error) {
console.error(`❌ Backup failed: ${error.message}`);
// 실패 알림 전송
throw error;
}
}
}
// 사용 예시
const backup = new DatabaseBackup(
{
host: 'localhost',
database: 'myapp',
user: 'backup_user',
password: 'secure_password'
},
{
region: 'ap-northeast-2',
bucket: 'my-db-backups'
}
);
backup.runBackupWorkflow();
# ============================================
# PostgreSQL 백업 명령어
# ============================================
# 1. 논리적 백업 (pg_dump)
# 단일 데이터베이스 백업
pg_dump -h localhost -U postgres -d mydb > backup.sql
# 압축하여 백업
pg_dump -h localhost -U postgres -d mydb | gzip > backup.sql.gz
# Custom 포맷 (병렬 복원 가능)
pg_dump -h localhost -U postgres -d mydb -F c -f backup.dump
# 특정 테이블만 백업
pg_dump -h localhost -U postgres -d mydb -t users -t orders > tables.sql
# 2. 전체 클러스터 백업
pg_dumpall -h localhost -U postgres > all_databases.sql
# 3. 물리적 백업 (pg_basebackup) - PITR 지원
pg_basebackup -h localhost -U replication -D /backup/base -Ft -z -P
# 4. 복원
psql -h localhost -U postgres -d mydb < backup.sql
pg_restore -h localhost -U postgres -d mydb backup.dump
# ============================================
# MySQL 백업 명령어
# ============================================
# 1. 단일 데이터베이스 백업
mysqldump -h localhost -u root -p mydb > backup.sql
# 트랜잭션 일관성 보장 (InnoDB)
mysqldump -h localhost -u root -p \
--single-transaction \
--routines \
--triggers \
mydb > backup.sql
# 압축하여 백업
mysqldump -h localhost -u root -p mydb | gzip > backup.sql.gz
# 2. 전체 백업
mysqldump -h localhost -u root -p --all-databases > all_databases.sql
# 3. 바이너리 로그 위치 포함 (PITR용)
mysqldump -h localhost -u root -p \
--single-transaction \
--master-data=2 \
mydb > backup_with_position.sql
# 4. 복원
mysql -h localhost -u root -p mydb < backup.sql
gunzip < backup.sql.gz | mysql -h localhost -u root -p mydb
# ============================================
# 자동화 스크립트 (cron)
# ============================================
# /etc/cron.d/db-backup
# 매일 새벽 2시 풀 백업
0 2 * * * postgres /opt/scripts/daily_backup.sh
# daily_backup.sh 예시
#!/bin/bash
DATE=$(date +%Y%m%d)
BACKUP_DIR="/var/backups/postgresql"
S3_BUCKET="my-db-backups"
# 백업 생성
pg_dump -h localhost -U backup_user -d production \
-F c -f "$BACKUP_DIR/production_$DATE.dump"
# S3 업로드
aws s3 cp "$BACKUP_DIR/production_$DATE.dump" \
"s3://$S3_BUCKET/daily/production_$DATE.dump"
# 7일 이상 된 로컬 백업 삭제
find $BACKUP_DIR -name "*.dump" -mtime +7 -delete
echo "Backup completed: production_$DATE.dump"
"3-2-1 백업 규칙을 적용했습니다. 로컬에 7일치 풀 백업을 보관하고, S3에 30일, Glacier에 1년치를 보관합니다. RTO는 2시간, RPO는 1시간으로 WAL 아카이빙으로 PITR을 지원합니다."
"데이터가 100GB 넘으니까 매일 풀 백업은 비효율적입니다. 주 1회 풀 백업에 일일 증분 백업 조합으로 가죠. pg_basebackup으로 물리적 백업하고 WAL 아카이빙으로 특정 시점 복구가 가능하게요."
"백업은 있었는데 복원 테스트를 안 해봐서 당황했습니다. 이제부터 분기마다 DR 훈련으로 실제 복원 절차를 검증하고, 복원 소요 시간도 측정해서 RTO 충족 여부를 확인하겠습니다."
백업 파일이 손상되었거나 복원 절차에 문제가 있으면 실제 장애 시 복구 불가능합니다. 정기적으로 복원 테스트를 수행하세요.
서버 장애나 디스크 손상 시 백업도 함께 유실됩니다. 반드시 오프사이트(S3, 다른 리전)에 복사본을 보관하세요.
MyISAM 테이블이나 잘못된 옵션 사용 시 테이블 락이 걸립니다. InnoDB + --single-transaction 옵션으로 무중단 백업하세요.
3-2-1 규칙 준수, 백업 암호화(AES-256), 복원 테스트 자동화, 백업 모니터링 및 알림 설정, RPO/RTO 기반 백업 주기 결정.