개요 — 무엇을 하나
InfraGuard 는 원격 crontab 을 직접 쓰지 않습니다. 대신 SE 가 배치한 백업 스크립트(또는 백업 전용 서버의 pm2 러너)가 실행을 마친 뒤 결과를 앱으로 되돌려 보고합니다. 이 콜백이 백업 리포트 계약입니다.
POST {APP_URL}/api/v1/backup/report (인증: infr.apikey · 무상태 · 세션/CSRF 없음)
콜백을 받으면 App\Services\Backup\BackupReportService 가 세 가지를 원자적으로 수행합니다.
infr_backup_runinsert — 신규 SOT(복구지점 시계열).status(done/failed/…)·bytes·artifact_path·checksum으로 stats-design 레거시backup_history의 결함(성공/실패 구분 불가, 용량·아티팩트 부재)을 해소합니다.- 레거시
backup_history호환 write — Overview 의 백업 성공률·요약 KPI 가 계속 동작하도록 (legacy 커넥션에) 함께 기록합니다. 실패해도 흡수(신규 write 는 이미 성공). infr_backup_job갱신 —last_run_at·last_status·next_run_at(스케줄"N U"로 재계산)을 업데이트합니다.
이 계약은 "실행"이 아니라 "보고"만 담당합니다. 어떻게 실행되는지(스크립트 생성)는 백업 스크립트 빌더, 무엇을 언제 백업할지는 백업 스케줄 보드를 보세요. 결과가 쌓이는 화면은 Runs 이고, 발생한 복구지점으로 복원 검증은 복구 & 검증에서 합니다.
언제 쓰나
- 백업 전용 서버 1대 + pm2 러너로 여러 잡을 중앙에서 돌리고 싶을 때 →
scripts/infr_backup_runner.cjs배포(권장). - 각 호스트에 개별 스크립트 + crontab 라인을 배치하는 방식일 때 → 스크립트 말미에 report 를 호출하는 한 줄을 넣습니다.
- 이미 도는 백업 파이프라인이 있고 결과만 InfraGuard 에 흘려보내고 싶을 때 →
curl한 줄이면 됩니다.
화면 구성
이 항목은 화면이 아니라 통합 계약입니다. 관련 UI 는 세 곳입니다.
| 위치 | 하는 일 | 등급 |
|---|---|---|
Administration ▸ API 발급 (/admin/api-keys) |
콜백 인증용 API 키 발급(평문 1회 노출)·비활성/삭제 | admin(≥10) |
| Backup ▸ Runs | 콜백이 적재한 infr_backup_run 목록·성공/실패·소요 스파크라인 |
staff(≥2) |
| Backup ▸ Script Builder | report 호출 한 줄(+ INFR_* emit)을 포함한 스크립트 생성 |
staff(≥2) |
사용법
1) 백업 스코프 API 키 발급
Administration ▸ API 발급에서 키를 발급합니다. 발급 시 평문 전체 키(igk_…)가 딱 한 번 노출되므로 그 자리에서 복사해 러너/스크립트에 넣습니다. DB 에는 해시(key_hash)만 저장됩니다.
인시던트 리포터와 섞지 말고 백업 전용 키를 따로 발급하세요.
/api/v1/backup/report는 append-only 라 스코프를 강제하지 않지만(활성 키면 통과), 키를 분리하면 유출 시 폐기 반경(blast radius)을 좁힐 수 있습니다.
2-A) pm2 러너로 중앙 실행 (권장)
scripts/infr_backup_runner.cjs 는 백업 전용 서버에서 pm2 스케줄(단발) 로 매 분 재기동되며, 중앙 MySQL 에서 실행 대상(due) 잡을 골라 최신 스크립트를 돌리고 결과를 자동 보고합니다.
- due 선정:
is_run=1 AND status='active' AND (next_run_at IS NULL OR next_run_at <= NOW())인 잡의 최신infr_generated_script.body. 스크립트 없는 잡은 JOIN 으로 제외. - 실행: 스크립트를 임시
.sh(0700)로 써서SHELL로 spawn → 실제 종료코드로 성공/실패 판정(0=done). 임시파일은 항상 삭제. - 결과 파싱: 스크립트 표준출력의
INFR_ARTIFACT/INFR_BYTES/INFR_CHECKSUM라인. - 보고:
POST /api/v1/backup/report(application/x-www-form-urlencoded,X-Infr-Api-Key헤더, 8s 타임아웃).
파일 상단 CONFIG 블록만 채웁니다(env 를 읽지 않음).
const CONFIG = {
DB_HOST: '203.0.113.10', // 중앙 MySQL(백업 잡·스크립트를 읽어옴)
DB_PORT: '3306',
DB_USER: 'infraguard', // 읽기 권한이면 충분
DB_PASSWORD: 'YOUR_DB_PASSWORD',
DB_NAME: 'design',
APP_URL: 'https://infraguard.example.com', // 리포트 대상(앱)
INFR_API_KEY: 'igk_YOUR_BACKUP_KEY', // API 발급의 백업 스코프 키
SHELL: '/bin/sh',
};
pm2 등록(단발 · 매 분 재기동):
npm i mysql2 # 유일한 npm 의존(mysqldump/rsync/ssh 는 OS 바이너리)
pm2 start infr_backup_runner.cjs --name infr-backup-runner \
--no-autorestart --cron-restart "* * * * *"
pm2 save
실제 백업 주기는 각 잡의
schedule(→next_run_at)이 결정합니다. 러너는 매 분 깨어나 due 인 것만 골라 실행합니다. 러너 자체는 상주하지 않고 매 실행이 새 프로세스(DB 연결 1개 open/close · 누수 없음)입니다.
2-B) 스크립트가 직접 report 호출
러너를 쓰지 않는다면, 대상 호스트의 백업 스크립트 말미에서 직접 콜백을 부릅니다. 빌더 골격은 이 부분을 자동으로 넣어 줍니다.
#!/bin/sh
set -eu
OUT="/data/backup/design_$(date +%Y%m%d%H%M).sql"
mysqldump --defaults-extra-file=/etc/infr/design.cnf --opt --routines --events design > "$OUT"
# ── 러너/빌더가 파싱하는 결과 emit 라인 ──
echo "INFR_ARTIFACT=$OUT"
echo "INFR_BYTES=$(wc -c < "$OUT")"
echo "INFR_CHECKSUM=$(sha256sum "$OUT" | cut -d' ' -f1)"
# ── report 콜백(러너를 쓰지 않는 경우 직접 호출) ──
curl -s -X POST "https://infraguard.example.com/api/v1/backup/report" \
-H "Authorization: Bearer igk_YOUR_BACKUP_KEY" \
-d "job_id=12&status=done&backup_type=db&bytes=$(wc -c < "$OUT")&duration_sec=$SECONDS&artifact_path=$OUT"
3) 연결 확인
배포 후 키·연결을 확인하려면 ping 을 먼저 두드립니다.
curl -s https://infraguard.example.com/api/v1/ping \
-H "Authorization: Bearer igk_YOUR_BACKUP_KEY"
# → {"ok":true, ...}
report 계약 (요청 필드)
폼 인코딩 또는 JSON 바디로 보냅니다. 서버가 검증·정규화합니다.
| 필드 | 필수 | 타입/제약 | 설명 |
|---|---|---|---|
job_id |
✔ | integer ≥ 1 | 대상 infr_backup_job.id. 없으면 422. |
status |
string ≤ 16 | running·done·failed·skipped. 그 외 값은 done 으로 정규화. 기본 done. |
|
backup_type |
string ≤ 16 | db·web·volume·custom. 그 외는 db. (레거시 write 는 volume/custom→db) |
|
bytes |
integer ≥ 0 | 아티팩트 용량. | |
artifact_path |
string ≤ 255 | 산출물 경로. artifact_name 은 basename() 으로 자동 파생. |
|
checksum |
string ≤ 80 | 무결성 해시(체크섬). | |
duration_sec |
integer ≥ 0 | 소요(초). started_at = finished_at − duration 으로 역산. |
|
target_path |
string ≤ 255 | 저장 디렉터리(레거시 호환용). 비면 dirname(artifact_path). |
|
server_ip |
string ≤ 45 | 실행 호스트 IP. 비우면 서버가 요청 IP 로 보정. | |
has_schedule |
bool | 스케줄 유래 여부(기본 1). | |
message |
string ≤ 2000 | 실패 사유·로그 꼬리(러너는 stderr 마지막 400자). | |
backup_id |
integer | 레거시 backup_history 참조(선택). |
응답
성공 시 201 Created:
{ "ok": true, "run_id": 8123, "status": "done", "key": "backup-runner" }
멱등(중복 재시도 방어)
같은 job_id + artifact_path + status 조합이 최근 2분 내 이미 기록됐으면 새 행을 만들지 않고 기존 run 을 그대로 반환합니다. cron/러너 재시도로 인한 이중 적재를 막습니다.
실패 처리와 재시도
- 스크립트 종료코드 ≠ 0 →
status=failed로 보고. 잡은paused가 아니면 활성 유지(다음 tick 재시도). - 러너의 report POST 가 실패(네트워크·타임아웃)하면
next_run_at이 갱신되지 않으므로(그 갱신은 앱이 콜백 수신 시 수행) 다음 tick 에 같은 잡이 다시 due 로 잡혀 재시도됩니다.
데이터 소스
| 대상 | 커넥션 | 역할 |
|---|---|---|
infr_backup_run |
기본(infr) | 신규 복구지점 SOT(insert). Runs/Recovery 화면 원천. |
infr_backup_job |
기본(infr) | last_run_at·last_status·next_run_at 갱신. |
infr_generated_script |
기본(infr) | 러너가 실행하는 최신 스크립트 본문. |
backup_history |
legacy | 레거시 호환 write(Overview KPI 유지). 실패는 흡수+로그. |
infr_api_key |
기본(infr) | 콜백 인증(key_hash 상수시간 대조)·사용 흔적(last_used_at·use_count). |
관련 파일:
app/Http/Controllers/Api/BackupReportController.php— 검증·server_ip보정·201 응답.app/Services/Backup/BackupReportService.php— 3중 write·정규화·멱등·잡 갱신.app/Http/Middleware/InfrApiKey.php—infr.apikey인증·레이트리밋.app/Support/ScheduleGrammar.php—"N U"파싱·next_run_at계산.scripts/infr_backup_runner.cjs— pm2 중앙 러너.routes/api.php—/api/v1/*라우트 등록(bootstrapwithRouting(api:)).
권한
| 작업 | 게이트 |
|---|---|
POST /api/v1/backup/report |
infr.apikey(활성 키면 통과 · 스코프 미강제 · 키별 분당 120회 레이트리밋) |
API 키 발급/비활성/삭제 (/admin/api-keys) |
admin(≥10) — infr.level:10 + 컨트롤러 self-gate + 감사 |
키 평문 복구(러너 메시업 주입, secret_enc) |
level≥5 + 감사 |
| Backup 화면(Board/Runs/Recovery/Builder) 조회·쓰기 | staff(≥2) — infr.level:2 |
인증 헤더는 둘 중 하나: Authorization: Bearer igk_… 또는 X-Infr-Api-Key: igk_…. 저장된 해시와 상수시간으로 비교하며, 키 모델을 request attribute 로 stash 합니다.
스크립트·크론·로그에 DB 비밀번호나 API 키를 평문으로 남기지 마세요. mysqldump 는
--defaults-extra-file(0600), rsync/ssh 는 신뢰망 키를 쓰고,INFR_API_KEY는 러너 CONFIG 또는 관리자 코드 메시업 주입(감사)으로만 전달합니다.
문제해결 · FAQ
| 증상 | 원인 · 조치 |
|---|---|
401 {"error":"missing_api_key"} |
헤더 누락. Authorization: Bearer igk_… 또는 X-Infr-Api-Key 확인. |
401 {"error":"invalid_api_key"} |
키가 비활성이거나 해시 불일치. 재발급하거나 활성 상태 확인. |
429 {"error":"rate_limited"} |
키별 분당 120회 초과. Retry-After 초 뒤 재시도. 여러 잡을 한 러너 tick 에 몰지 말 것. |
422 {"error":"validation"} |
job_id 누락/비정수 등. 필드 표 참조. |
러너 로그 mysql2 미설치 |
백업 서버에서 npm i mysql2. |
러너 로그 중앙 DB … 미설정 → 종료 |
CONFIG 의 DB_HOST/DB_USER/DB_NAME 빈 값 또는 {{TOKEN}} 미치환. |
러너 로그 리포트 전송 실패 → 다음 tick 재시도 |
APP_URL/INFR_API_KEY 확인·방화벽·TLS. 다음 tick 에 재시도됨. |
| Runs 에 결과가 안 보임 | job_id 가 실제 존재하는지, 2분 멱등 창에 중복 흡수됐는지 확인. |
| Overview 백업 KPI 만 비어 있음 | 레거시 backup_history write 실패(흡수+경고 로그). storage/logs 에서 backup_history compat write failed 확인. |
| 같은 백업이 매 tick 반복 실행 | report POST 가 앱에 도달하지 못해 next_run_at 미갱신. 러너→앱 네트워크 점검. |
관련 항목
- Administration ▸ API 발급 — 콜백 인증 키 발급·연동 코드.
- 백업 스크립트 빌더 — report 호출·
INFR_*emit 을 포함한 스크립트 생성. - 백업 스케줄 보드 — 무엇·언제·어디에·다음 실행(
next_run_at). - 복구 & 검증 — 콜백이 만든 복구지점으로 복원 리허설.
- 백업 볼트 — 3-2-1 저장 대상·보존.
- 감사 로그 — 키 발급·복구 열람 이력.
