feat: DB 백업 시스템 구축 (Phase 1,2,4)

- Phase 1: backup.conf.example + sam-db-backup.sh 백업 스크립트
- Phase 2: BackupCheckCommand + StatMonitorService.recordBackupFailure()
- Phase 2: routes/console.php에 db:backup-check 05:00 스케줄 등록
- Phase 4: SlackNotificationService 생성 (웹훅 알림)
- Phase 4: BackupCheckCommand/StatMonitorService에 Slack 알림 연동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 08:33:19 +09:00
parent 57d9ac2d7f
commit fb0155624f
6 changed files with 530 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use App\Services\SlackNotificationService;
use App\Services\Stats\StatMonitorService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class BackupCheckCommand extends Command
{
protected $signature = 'db:backup-check
{--path= : 백업 경로 오버라이드}';
protected $description = 'DB 백업 상태를 확인하고 이상 시 알림을 생성합니다';
public function handle(StatMonitorService $monitorService): int
{
$this->info('DB 백업 상태 확인 시작...');
$statusFile = $this->option('path')
? rtrim($this->option('path'), '/') . '/.backup_status'
: env('BACKUP_STATUS_FILE', '/data/backup/mysql/.backup_status');
$errors = [];
// 1. 상태 파일 존재 여부
if (! file_exists($statusFile)) {
$errors[] = '백업 상태 파일 없음: ' . $statusFile;
$this->reportErrors($monitorService, $errors);
return self::FAILURE;
}
$status = json_decode(file_get_contents($statusFile), true);
if (json_last_error() !== JSON_ERROR_NONE) {
$errors[] = '상태 파일 JSON 파싱 실패: ' . json_last_error_msg();
$this->reportErrors($monitorService, $errors);
return self::FAILURE;
}
// 2. last_run이 25시간 이내인지
$lastRun = strtotime($status['last_run'] ?? '');
if (! $lastRun || (time() - $lastRun) > 25 * 3600) {
$lastRunStr = $status['last_run'] ?? 'unknown';
$errors[] = "마지막 백업이 25시간 초과: {$lastRunStr}";
}
// 3. status가 success인지
if (($status['status'] ?? '') !== 'success') {
$errors[] = '백업 상태 실패: ' . ($status['status'] ?? 'unknown');
}
// 4. 각 DB 백업 파일 크기 검증
$minSizes = [
'sam' => (int) env('BACKUP_MIN_SIZE_SAM', 1048576),
'sam_stat' => (int) env('BACKUP_MIN_SIZE_STAT', 102400),
];
$databases = $status['databases'] ?? [];
foreach ($minSizes as $dbName => $minSize) {
if (! isset($databases[$dbName])) {
$errors[] = "{$dbName} DB 백업 정보 없음";
continue;
}
$sizeBytes = $databases[$dbName]['size_bytes'] ?? 0;
if ($sizeBytes < $minSize) {
$errors[] = "{$dbName} 백업 파일 크기 부족: {$sizeBytes} bytes (최소 {$minSize})";
}
}
// 결과 처리
if (! empty($errors)) {
$this->reportErrors($monitorService, $errors);
return self::FAILURE;
}
$this->info('✅ DB 백업 상태 정상');
Log::info('db:backup-check 정상', [
'last_run' => $status['last_run'],
'databases' => array_keys($databases),
]);
return self::SUCCESS;
}
private function reportErrors(StatMonitorService $monitorService, array $errors): void
{
$errorMessage = implode("\n", $errors);
$this->error('❌ DB 백업 이상 감지:');
foreach ($errors as $error) {
$this->error(" - {$error}");
}
// stat_alerts에 기록
$monitorService->recordBackupFailure(
'[backup] DB 백업 이상 감지',
$errorMessage
);
// Slack 알림 전송
app(SlackNotificationService::class)->sendBackupAlert(
'DB 백업 이상 감지',
$errorMessage
);
Log::error('db:backup-check 실패', ['errors' => $errors]);
}
}