백업·복구¶
/admin/backup admin
플랫폼과 스택의 상태를 S3 호환 저장소로 떠 두고, 필요할 때 되돌립니다. 화면 설명 그대로 "백업을 실행하고 지난 실행을 확인합니다".
복구는 화면이 없습니다
백업은 화면에서 하지만, 복구는 아직 UI 가 없습니다. web/src/ 아래에 복구
화면·훅·API 클라이언트가 전혀 없고, POST /api/v1/admin/restores 를 직접
호출해야 합니다. 절차는 아래에 적어 두었습니다.
백업하는 법¶
- 오른쪽 위 백업 실행 을 누릅니다.
- 백업 대상 선택 창에서 범위를 고릅니다.
- 받아 갈 항목(아래 6개)을 체크합니다.
- 볼륨을 포함하면 확인 문구를 입력해야 합니다.
- 백업 시작.
백업은 동기로 끝납니다. 요청이 201 로 돌아오는 시점에 이미 작업이 끝나 있습니다. 화면은 진행 중인 실행이 있을 때만 5초마다 목록을 새로 읽습니다.
백업 범위 세 가지¶
| 선택 | 모드 값 | 담는 것 |
|---|---|---|
| 전체 — 플랫폼 + 모든 스택 | 백업 세트 | 플랫폼 1건 + 스택마다 1건 |
| 스택 하나 (네임스페이스 표시) | full |
플랫폼 + 그 스택 |
| 플랫폼만 (스택 없음) | platform_only |
플랫폼 DB·Keycloak DB |
스택을 하나 고르면 기본으로 첫 스택이 선택되고, 항목 6개가 모두 체크돼 있습니다. 플랫폼만 을 고르면 스택 전용 항목 4개는 비활성화되고, 전체 를 고르면 항목 선택 자체가 서버 판단으로 넘어갑니다.
받아 가는 항목 여섯 개¶
| 항목 | 무엇이 들어가나 |
|---|---|
platform_db |
Nullus 플랫폼 PostgreSQL (pg_dump --format=custom) |
keycloak_db |
Keycloak 자체 데이터베이스 |
openbao_kv |
스택별 OpenBao KV 를 논리 재귀 내보내기 (raft 스냅샷 아님) |
ns_resources |
네임스페이스 리소스 — Deployment·StatefulSet·Service·ConfigMap·Secret·Ingress·Gateway/HTTPRoute·ESO·Argo CD·Prometheus 오퍼레이터 객체 + 그 네임스페이스 Helm 릴리스가 소유한 클러스터 범위 객체(CRD·ClusterRole·ClusterRoleBinding) + Argo CD 대상 네임스페이스와 레지스트리 pull Secret. PVC 는 일부러 제외(볼륨 쪽이 맡습니다) |
shared_resources |
마지막 스택과 함께 회수되는 클러스터 공용 자원 — Gateway API CRD·GatewayClass, 내부 CA(ClusterIssuer·Certificate·Secret) |
volume |
네임스페이스의 모든 PVC 를 헬퍼 파드로 tar 떠 가기 |
Helm 릴리스 상태는 따로 담지 않습니다
릴리스 Secret(sh.helm.release.v1.*)이 ns_resources 에 함께 들어가고,
차트 이름·버전·리비전은 매니페스트에 기록됩니다. cert-manager 와
metrics-server 릴리스 자체는 백업하지 않고 복구 때 다시 설치합니다.
볼륨을 포함하면 서비스가 멈춥니다¶
볼륨 항목이 들어가면 그 네임스페이스의 Deployment·StatefulSet 을 전부 레플리카 0 으로 내리고, 파드가 사라진 뒤 아카이브하고, 끝나면 되돌립니다. 실패해도 되돌립니다.
그래서 화면에 주황색 경고가 뜨고 확인 문구를 요구합니다.
| 범위 | 입력할 문구 |
|---|---|
| 스택 하나 | 그 네임스페이스 이름 |
| 전체 | all |
서버도 같은 규칙을 다시 확인하고, 틀리면 실행을 만들기 전에
400 BACKUP_CONFIRM_REQUIRED 로 거절합니다. 실제 멈춘 시간은 표의 정지 시간
컬럼에 기록됩니다.
이력 표¶
| 컬럼 | 내용 |
|---|---|
| 시작 | 실행이 만들어진 시각 |
| 대상 | 스택 이름, 플랫폼이면 플랫폼. 세트 소속이면 세트 <앞 8자리> 배지가 붙습니다 |
| 모드 | full · platform_only · stack_only |
| 크기 | total_bytes 를 사람이 읽는 단위로 |
| 정지 시간 | 볼륨 때문에 멈춘 초. 없으면 — |
| 상태 | pending·running(파랑) · succeeded(초록) · partial(주황) · failed(빨강) |
partial 은 일부 항목만 성공한 상태입니다. 초록으로 칠하지 않은 건 의도입니다 —
전부 받아 온 백업과 구분해야 하니까요. 실패 사유는 상태 옆에 작은 글씨로 붙습니다.
정합성 확인 (verify)¶
응답은 {backup_run_id, ok, checked_count, schema_check, missing_objects[], checksum_fails[]} 입니다.
verify 는 암호를 풀어 보지 않습니다
각 산출물에 대해 저장소에 객체가 있는지, 업로드 때 메타데이터에 적어 둔 sha256 이 DB 기록과 같은지, 그리고 스키마 버전이 복구 가능한지를 봅니다. 내려받아 다시 해시하거나 복호화하지는 않습니다. 그래서 나중에 저장소에서 바이트가 깨지거나 잘린 경우는 잡지 못합니다. 그건 실제 복구 때 암호를 풀면서 드러납니다(청크마다 "마지막" 표시를 AAD 로 묶어 두어 잘린 암호문은 열리지 않습니다).
백업 세트¶
하나의 백업 실행에는 플랫폼 또는 스택 하나만 담깁니다 — 스택 둘을 한 실행에 담지 않습니다. 스택마다 클러스터와 금고가 따로이기 때문입니다.
전체 를 고르면 set_id 를 공유하는 실행 여러 개가 만들어집니다.
- 순서: 플랫폼(
platform_only) 먼저, 그다음 완료된 스택마다 하나(stack_only+ 그 스택 금고) - 차례로, 동기로 돕니다. 한 건이 실패해도 나머지는 계속합니다
- 세트 상태는 저장하지 않고 그때그때 계산합니다 — 하나라도 진행 중이면
running, 전부 실패면failed, 전부 성공이면succeeded, 그 외partial
복구¶
복구는 API 로만 합니다.
curl -X POST "$API/api/v1/admin/restores" \
-H 'Content-Type: application/json' \
-d '{"backup_run_id":"<백업 ID>","confirm":"<같은 백업 ID>","namespace":"<스택 네임스페이스>"}'
confirm에는 백업 ID 를 그대로 넣습니다. 다르면400 RESTORE_CONFIRM_REQUIRED.- 백업에
stack_id가 있고 모드가platform_only가 아니면namespace가 필수입니다. - 진행 상태는
GET /api/v1/admin/restores/{id}로 봅니다.
복구가 하는 일 (실제 순서)¶
- 선행 조건 —
ENCRYPTION_KEY(32바이트), 백업 봉인 키(32바이트), 목적지 자격증명, 목적지 도달 확인. 하나라도 없으면 아무것도 건드리지 않고 멈춥니다 - 백업이
succeeded또는partial인지 확인 - 산출물 정합성 사전 점검 (verify 와 같은 방식)
- 스키마 확인 — 현재 DB 가 dirty 거나, 백업 버전이 현재보다 높으면 중단
- 차트 버전 차이는 경고로만 남기고 막지 않습니다
- 스택 쪽 — 네임스페이스 확보 → 내부 CA Secret 을 먼저 적용 → cert-manager·metrics-server 재설치 → 공용 자원을 없는 것만 적용 → 매니페스트의 크기·StorageClass 로 PVC 재생성 → 볼륨 tar 복원 → 네임스페이스 리소스 적용
- 플랫폼 쪽 —
pg_restore --clean --if-exists. 백업 이력 테이블은 복원하지 않습니다. 이어서 Keycloak DB(실패해도 치명적이지 않음) - 워크로드를 매니페스트의 원래 레플리카로 되돌림
- 스택 금고 KV 를 그 뒤에 가져오기 (기본 2분까지 10초 간격 재시도, 실패해도 복구는 성공)
- 참조 정합성 점검 →
integrity_report - 게이트웨이 주소 재고정 — 러너·Argo CD 의
hostAliases를 지금 게이트웨이 주소로 고쳐 씀
복구는 기존 것을 지우지 않습니다
네임스페이스·릴리스·PVC 를 삭제하지 않습니다. 네임스페이스가 있는지 확인한 뒤 그 위에 덮어씁니다. 그래서 백업 시점에 없던 리소스는 복구 뒤에도 남습니다 — 깨끗한 상태로 되돌리려면 네임스페이스를 먼저 지우세요.
dangling 은 무엇인가¶
복구 결과의 integrity_report.dangling[] 은 복원된 데이터베이스가 가리키는 비밀
경로 중, 복원된 금고에 실제로 없는 것의 목록입니다({token_source_id, path}).
조직의 토큰 출처 경로를 하나씩 금고에 있는지 물어보고, 없는 것을 모읍니다.
checked_paths 는 확인한 경로 수입니다. 경고일 뿐 복구는 성공으로
보고되며, 해당 토큰 출처는 다시 등록해야 합니다.
세트 복구¶
플랫폼을 먼저 복구합니다. 플랫폼이 실패하면 "플랫폼 복구 실패 — 스택은 되돌리지 않았습니다" 로 전체를 멈춥니다. 스택들은 서로 독립적으로 이어 갑니다.
저장소와 암호화¶
| 항목 | 값 |
|---|---|
| 저장 위치 | 클러스터 밖 S3 호환 버킷 하나 |
| 객체 키 | [<prefix>/]backup-<실행ID>/<이름> |
| 암호화 | 스트리밍 AES-256-GCM, 1 MiB 청크, 헤더 NBK1 |
| 압축 | 없음 — 볼륨은 tar -cpf - 그대로 (pg_dump 내부 압축만 있음) |
| 로컬 디스크 | 쓰지 않음 (파이프로 바로 업로드) |
| 키 | 하나뿐 (backup.seal_key, 32바이트). ENCRYPTION_KEY 및 DB 비밀번호와 달라야 하고, 같으면 서버가 뜨지 않습니다 |
멈추기 전에 목적지 사전 점검을 합니다 — 버킷 존재 확인에 그치지 않고 탐침 객체를 실제로 PUT·DELETE 해 봅니다.
버킷에 매니페스트 파일은 없습니다
매니페스트는 데이터베이스 컬럼에만 있고 GET /api/v1/admin/backups/{id} 로
꺼냅니다. 버킷만 들고 다른 곳에서 복구할 수는 없습니다. 산출물 이름에도
.enc·.zst 가 붙지 않습니다 — 압축하지 않고, 암호화 여부는 이름에
드러나지 않습니다.
예약 백업¶
구현돼 있지만 기본값은 꺼짐입니다.
| 항목 | 기본값 |
|---|---|
backup.schedule.enabled |
false |
| 주기 | 24시간 |
| 1회 제한 시간 | 4시간 |
| 보존 | 일 7 · 주 4 · 월 3 |
앞 백업이 아직 돌고 있으면 그 틱을 건너뜁니다. 보존 정책은 객체만 지우고 이력 행은 남기며, 가장 최근 성공 백업은 절대 지우지 않습니다. 보존은 예약 백업에서만 돌고, 수동 백업에는 적용되지 않습니다.
더 읽을 것¶
- 백업·복구 아키텍처 — 설계 도면과 구성도
지금 없는 것¶
| 없는 것 | 비고 |
|---|---|
| 백업 삭제 API | DELETE /backups/:id 가 없습니다. 보존 정책만 지웁니다 |
| 감사 로그 | 백업·복구는 audit_logs 에 남지 않습니다 — 누가 언제 복구했는지 추적할 수 없습니다 |
| 복구 화면 | API 전용 |
| 복구 후 스모크 검사 | 없습니다. 복구가 succeeded 라도 로그인·git clone·이미지 pull·파이프라인 재실행은 직접 확인하세요 |
| 키 에스크로·회전 | 코드에는 없습니다. 봉인 인터페이스가 BYOK 교체 지점으로만 열려 있습니다 |