Nullus 백업/복구 아키텍처
플랫폼 메타데이터부터 설치된 OSS 의 내부 데이터까지 한 시점으로 얼려 클러스터 밖에 두고, 그 시점으로 되돌리는 구조. 이 모듈의 값어치는 기능이 아니라 순서에 있다 — 바꾸면 조용히 깨지는 순서들이 있고, 그것을 코드와 테스트로 고정한 것이 설계의 대부분이다.
01무엇을 지키는가
백업 대상은 다섯 갈래다. 갈래마다 저장 위치와 뜨는 방법이 다르고, 빠졌을 때 드러나는 시점도 다르다 — 대부분은 복구를 시도할 때에야 드러난다.
| 컴포넌트 | 무엇 | 어떻게 | 빠지면 |
|---|---|---|---|
platform_db |
Nullus 메타데이터 — 스택 · 파이프라인 · 클러스터 | pg_dump |
복구 후 플랫폼이 자기 스택을 모른다 |
keycloak_db |
계정과 권한 | pg_dump |
복구 후 로그인할 수 없다 |
openbao_kv |
토큰 · 자격증명 (스택마다 하나) | KV 재귀 순회 논리 export | 도구들이 서로 인증하지 못한다 |
ns_resources |
배선 — Deployment · Secret · CRD · ClusterRole · Argo CD Application | kubectl get -o json + 정리 |
복구가 succeeded 인데 스택이 뜨지 않는다 |
volume |
OSS 내부 데이터 — Git 저장소 · 빌드 이력 · 이미지 blob | PVC 열거 → 헬퍼 파드가 tar | 도구가 빈 디스크를 보고 재초기화한다 |
ns_resources 의 종류 목록은 아직 손으로 관리하며,
지금까지 나온 결함 대부분이 그 목록에서 빠진 것이었다 —
ESO 배선 · CRD · ClusterRole · Argo CD Application 이 차례로 그랬다.
§8 이 그 뿌리를 다룬다.
02모듈 구조
Clean Architecture 를 따른다. 의존은 항상 안쪽을 향하고, 바깥은 포트를 구현할 뿐이다. 도메인과 유스케이스는 클러스터도 DB 도 없이 실행된다 — 순서 검증 같은 이 모듈의 핵심 규칙을 인프라 없이 고정하기 위해서다.
어댑터는 요청 시점에 만든다
백업 대상 스택은 등록된 워크로드 클러스터에 있다. 기동 시점에는 그
kubeconfig 를 모르므로, wiring.go 가 스택 ID → 클러스터 ID → kubeconfig 를
풀어 그때 어댑터 묶음을 만들고 캐시한다.
모듈 경계를 넘지 않는다
토큰 회전 일시정지처럼 admin 모듈이 필요한 일은
adapter/bridge/ 를 통해서만 닿는다. 회전이 DB 와 금고를 5분마다 함께
고쳐 쓰므로, 멈추지 않으면 두 산출물 사이에 시점 어긋남이 생긴다.
03백업 흐름
호박색 구간이 서비스가 멈춰 있는 동안이다. 그 바깥은 전부 무중단으로 돈다. 이 배치가 이 모듈의 설계 그 자체다 — 무엇을 창 안에 넣지 않을 것인가로 정지 시간이 정해진다.
-
00
목적지 검사 무중단
연결 · 인증 · 쓰기 · 용량. 필요 용량은 정적 계산이 아니라 실행 시점 PVC 조회로 구한다.
-
01
리소스 덤프 무중단
워크로드가 살아 있을 때 떠야 실제 상태가 담긴다. 정지 뒤에 뜨면 replica 0 인 모습이 담긴다.
-
02
워크로드 정지
replica 를 0 으로. 원래 값은 매니페스트에 적어 둔다 — 복구의 필수 입력이다.
-
03
볼륨 아카이브
헬퍼 파드가 PVC 를 마운트해 tar 로 흘려보낸다. 봉인과 업로드가 스트리밍으로 이어진다.
-
04
워크로드 재개 defer 보장
실패해도 반드시 돈다. 최악은 백업하려다 서비스를 못 살리는 것이다.
-
05
금고 → DB 무중단
순서가 금고 먼저다. 그러면 "DB 는 아는데 금고엔 없다" 방향의 불일치만 남고, 그것은 복구 9단계의 참조 정합성 검사가 잡는다. 반대 방향은 잡히지 않는다.
-
06
매니페스트 작성 무중단
산출물 목록 · 볼륨 크기 · 원래 replica · 차트 버전 · 암호화 키 ID. 이것만 암호화하지 않는다.
04복구 흐름
복구에는 정지 창이 없다 — 대상이 이미 비어 있거나 죽어 있기 때문이다. 대신 순서가 훨씬 엄격하다. 뒤집으면 복구가 "성공" 을 반환하고도 쓸 수 없는 상태가 된다.
-
00
전제 확인
암호화 키 · 봉인 키 · 목적지 자격증명. 하나라도 없으면 아무것도 하지 않고 멈춘다 — 키 없이 진행하면 "복구된 것처럼 보이는 망가진 상태" 가 남는다.
-
01
무결성 검증
산출물이 열리는지, 조각이 잘리지 않았는지.
-
02
스키마 · 차트 버전 대조
대상에 다른 버전이 깔려 있으면 기동 시 마이그레이션이 돌아 되돌릴 수 없다. 버전은 알리되 막지는 않는다 — 버전을 올린 뒤 데이터만 회수하는 길이 있다.
-
03
PVC 생성 → 볼륨 복원 → 리소스 적용
PVC 는 볼륨 경로가 단독으로 소유한다. 리소스 덤프에도 PVC 가 있으면 같은 객체를 두 경로가 소유해
spec is immutable로 죽는다. -
04
DB → 금고
컨트롤 플레인 쪽. 스택 복구와 독립적으로 돈다.
-
05
워크로드 기동 반드시 마지막
순서를 뒤집으면 도구들이 빈 디스크를 보고 재초기화한다. Gitea 가 새 인스턴스가 되는 순간 복원은 무의미해진다.
-
06
참조 정합성 검사
토큰 소스가 가리키는 금고 경로가 실제로 있는지. 경고하되 중단하지 않는다.
succeeded 는 "단계가 돌았다" 는 뜻이다. "스택이 동작한다" 가 아니다.
실제로 RestoreRun.status = succeeded 인데 Gitea · Harbor · Jenkins 가
CreateContainerConfigError 로 9분 넘게 멈춰 있던 적이 있다 —
ESO 배선과 ClusterRole 이 백업에서 빠져 있었기 때문이다.
자동 복구 경로에는 아직 스모크 검증이 없다.
05산출물과 암호화
목적지 배치
<bucket>/ backup-<run-id>/ manifest.json ← 유일한 평문 platform-db.dump keycloak-db.dump openbao-kv.json namespace-resources.yaml volumes/ jenkins.tar gitea-shared-storage.tar harbor-registry.tar … PVC 마다 하나
목적지는 클러스터 밖 S3 호환 오브젝트 스토리지다. 자격증명은 스택 금고가 아니라 컨트롤 플레인 설정에서만 온다 — 금고에서 조달하면 스택이 죽을 때 백업본이 멀쩡해도 가져올 수 없는 순환이 생긴다.
스트리밍 AES-256-GCM
공용 pkg/crypto 는 []byte 전체를 받아 수십 GB 에 쓸 수 없다.
같은 알고리즘의 스트리밍 변형을 따로 두었다.
| 조각 크기 | 1 MiB |
| nonce | 랜덤 8B ‖ 조각번호 4B |
| AAD | 조각 번호 + 마지막 조각 여부 |
AAD 에 "마지막 조각인가" 를 넣는 것이 요점이다. 이것이 없으면 뒤가 잘린 백업본이 검증을 통과한다 — 조용히 짧아진 산출물은 가장 나쁜 종류의 손상이다.
06데이터 모델
| 테이블 | 역할 | 눈여겨볼 칼럼 |
|---|---|---|
backup_runs |
실행 하나 — 플랫폼 하나 또는 스택 하나 | scope TEXT[] · quiesce_started_at / _ended_at · manifest JSONB · set_id UUID(세트 꼬리표, NULL 이면 단독) |
backup_artifacts |
산출물 하나 (실행당 N) | component · location · checksum · encryption_key_id |
restore_runs |
복구 시도 | schema_check · integrity_report |
scope 가 mode 보다 우선한다
mode 는 preset 이고 실제로 무엇을 뜰지는 scope 가 정한다.
정지 창 여부도 scope 에 volume 이 있는지로 판단한다 —
볼륨을 뺀 full 백업은 무중단이고 확인 문구를 요구하지 않는다.
보존은 일 7 · 주 4 · 월 3
어떤 경우에도 가장 최근 성공본은 지우지 않는다. 보존 정책이 백업을 0 개로 만들 수 있으면 정책이 아니라 사고다.
stack_id 는 TEXT 다. Nullus 스택 ID 는 UUID 가 아니라
stk_352e9f68db06 형태이며, 이것을 UUID 로 선언했던 동안
스택을 대상으로 하는 백업이 행을 만드는 순간 죽었다(마이그레이션 000076).
"플랫폼 전체" 는 실행의 묶음이다. 한 실행은 플랫폼 하나 또는 스택 하나만 담는다 —
스택의 리소스·볼륨은 그 스택의 클러스터에서 뜨고 금고는 스택마다 있어서다. 그래서 플랫폼 + 조직의 모든 스택은
세트(POST /backups/sets)로 뜬다: 플랫폼 platform_only 실행 하나 +
설치 완료 스택마다 stack_only(+ 금고) 실행 하나가 같은 set_id 를 단다. 세트는 테이블이 아니라
꼬리표이고 FK 가 없다 — 이력 테이블은 어디도 참조하지 않는다(000092 의 원칙). 세트 복구
(POST /restores/sets)는 플랫폼 멤버를 먼저 되돌린 뒤 스택 멤버를 매니페스트의 namespace 로
하나씩 되돌린다 — 스택 복구가 플랫폼 DB 의 스택 기록으로 클러스터에 붙기 때문이다(설계 §6.7).
07실패 모드
이 모듈에서 가장 비싼 실패는 조용히 잘못되는 것이다. 실패했다는 사실이 늦게 드러나는 형태들을 골라 방어를 코드에 넣었다.
| 실패 | 왜 위험한가 | 방어 |
|---|---|---|
| 정지 후 목적지 실패 | 다운타임만 치르고 백업은 없다 | 정지 전 목적지 검사 |
| 재개 실패 | 백업하려다 서비스를 못 살린다 | defer 로 보장 |
| 산출물 뒤가 잘림 | 검증을 통과하는 손상 | 조각 AAD 에 마지막 여부 |
pg_dump 버전이 낮음 |
조용히 실패한다 | 서버와 major 대조 후 이미지 선택 |
| 없는 리소스 타입 조회 | 덤프 전체가 실패한다 | api-resources 로 먼저 물어본다 |
| 배선·권한 누락 | 복구가 succeeded 인데 스택이 죽어 있다 |
ESO CR · CRD · ClusterRole · Argo CD App 을 덤프에 포함 |
| 복구 볼륨을 잔여물로 오인 | 복구 직후 재설치가 막힌다 | PVC 의 backup.nullus.io/restored-from |
08한계와 다음
뿌리 하나: 복구 모델과 설치 모델이 다르다
설치는 Helm 릴리스로 이루어지는데, 복구는 네임스페이스 리소스를 apply 한다. 그래서 Helm 이 만드는 클러스터 범위 리소스(CRD · ClusterRole)와 컨트롤러가 재생성하는 리소스(ESO Secret · Argo CD 가 만드는 Deployment)가 그 사이로 빠진다.
지금까지 네 번 같은 이유로 결함이 났다. 빠진 것을 하나씩 세는 방식으로는 끝을 알 수 없다 — 다음 OSS 가 들어오면 또 난다.
후보 ① Helm 릴리스 재적용
리소스를 apply 하는 대신 릴리스를 다시 설치하고 데이터만 덮는다. Helm 이 만든 것은 Helm 이 아니까 종류 목록이 사라진다. 선행 조건 둘은 이미 갖췄다 — 차트 버전 기록과 복구 볼륨을 통과시키는 preflight.
차트가 자체 생성하는 비밀값이 걸림돌로 보였으나, 실측해 보니 그렇지 않다.
복원된 DB 를 못 읽게 만드는 것은 Harbor 의 secretKey 하나인데
그 값이 랜덤이 아니다 — 차트 기본값 not-a-secure-key 를
그대로 쓰고 있어 재설치해도 같다. 나머지 랜덤 값(CSRF_KEY ·
core.secret · JOBSERVICE_SECRET · REGISTRY_HTTP_SECRET)은
한 릴리스 안에서 함께 재생성되므로 서로 맞고,
REGISTRY_HTPASSWD 는 고정 비밀번호의 해시라 새로 만들어도 검증을 통과한다.
후보 ② 소유권 기반 열거
종류를 나열하는 대신 meta.helm.sh/release-namespace 나 스택 라벨이 붙은
모든 리소스를 훑는다. PVC 를 열거하는 것과 같은 발상이다.
먼저 고쳐야 할 것 — Harbor 가 공개된 기본 키로 돈다
secretKey 가 재설치에도 같다는 사실은 재적용 방식에는 다행이지만,
그 이유가 차트에 적힌 공개 기본값을 그대로 쓰고 있기 때문이다.
이 키는 Harbor 가 로봇 계정 자격증명과 레플리케이션 엔드포인트 비밀번호를
DB 에 암호화해 넣을 때 쓴다 — 우리 백업 산출물(volumes/harbor-database)을
손에 넣은 사람은 그 키로 풀 수 있다.
고치는 방법은 이미 저장소에 있다. 관리자 비밀번호가 그렇듯
금고를 SoT 로 두고 차트가 참조하게 하면 된다 —
차트는 existingSecretSecretKey 를 받는다. 최초 설치에서 강한 키를 만들어
금고에 넣고, 이후 모든 설치가 그것을 참조하면 보안 결함과 재적용의 전제가
한 번에 해결된다: 복구는 금고를 먼저 되돌리므로 재설치해도 같은 키가 쓰인다.
아직 재보지 않은 것
RTO 실측(371 MB 로 잰 값은 규모의 근거가 아니다) · 다중 노드에서 RWO 볼륨이 흩어질 때 · Argo CD 의 자동 동기화(리허설에서는 강제로 앞당겼다) · Service ClusterIP 미보존이 IP 고정 배선에 미치는 영향 · 차트 버전이 어긋난 대상으로의 복구.