넘겨받은 커머스의 데이터베이스에 백업과 보존 정책을 걸면서, 집행 순서를 바꿔 백업부터 켜기로 했다. 백업을 점검하니 되돌릴 지점이 사실상 없었다. Firebase Realtime Database(RTDB)의 마지막 백업은 14.5개월 전이었고, Firestore 는 백업도 PITR(시점 복구)도 꺼져 있었다. 지금 데이터가 손상되면 RTDB 는 1년 전으로, Firestore 는 아무 데로도 돌아갈 수 없다.
Table of contents
Open Table of contents
배경 — 정책 부재가 아닌 누락
같은 프로젝트의 Cloud SQL(PostgreSQL 17)은 backupConfiguration.enabled = True 였고, 감사 로그에 cloudsql.instances.automatedBackup 이 매일 찍혀 있었다. 이 대조군 덕분에 「이 조직은 백업을 하지 않는다」가 아니라 「RTDB 와 Firestore 만 빠졌다」로 판정할 수 있었다. 정책을 새로 세우는 일이 아니라 누락을 메우는 일이라는 뜻이다.
백업 버킷에는 객체가 3개뿐이었다. 1년 전 RTDB 전량 백업(gzip 6.77GB)과 규칙 파일, 그리고 백업과 무관한 남의 이미지 파일 하나다. 버킷에는 lifecycle 도, retention 도, versioning 도 없었다.
해결 — Firestore 세 설정과 재조회
Firestore 는 승인을 받아 세 가지를 켰다.
- 삭제 보호 —
DELETE_PROTECTION_ENABLED - PITR —
POINT_IN_TIME_RECOVERY_ENABLED, 보존 기간 1시간에서 7일로 - 스케줄 백업 — 매일, 3일 보존. 기존 스케줄이 0건인 것을 확인한 뒤 만들어 덮어쓴 것이 없다
세 가지 모두 명령이 성공했다는 메시지가 아니라, 설정을 다시 조회한 결과로 기록했다. 다만 삭제 보호가 실제로 삭제 명령을 거부하는지는 시험하지 않았고, 설정값 확인으로 갈음했다.
위험 지점 — 수명 정책을 먼저 걸면 생기는 일
RTDB 는 처음 세운 순서대로라면 「버킷에 30일 수명 정책을 먼저 걸고, 그다음 백업을 켠다」였다. 그런데 30일 lifecycle 은 앞으로 생길 객체만이 아니라 이미 30일을 넘긴 기존 객체에도 걸린다. 버킷의 객체 셋은 각각 439일·439일·288일이 지나 있었다. 정책을 거는 순간 그 자리에서 셋이 지워진다. 그중 둘은 지금 유일한 복구 지점이고, 나머지 하나는 우리에게 지울 권한이 없는 남의 파일이다.
그래서 RTDB 집행을 멈추고 순서를 다시 짰다. 후보는 셋이었다.
- 백업 설정에서 하위 폴더를 지정하고, lifecycle 을
matchesPrefix로 그 폴더에만 건다 - 버킷 lifecycle 대신 백업 설정 안의 30일 수명 정책을 쓴다
- 백업을 먼저 켜고 새 백업 1벌을 확인한 뒤에 lifecycle 을 건다
1번이 가장 깨끗하다고 본 이유는 기존 객체를 건드리지 않고 새 백업만 30일로 관리되기 때문이다. 반면 3번은 새 백업을 확보한 뒤라 안전하지만, 그 대가는 기존 객체 셋이 지워지는 문제가 그대로 남는다는 점이다. 1·2번 중 무엇이 되는지는 콘솔의 백업 탭에 폴더와 수명 정책 칸이 있는지에 달려 있었다. RTDB 백업 켜기는 콘솔에서만 할 수 있어 담당자가 직접 눌러야 하는 일이었다.
측정 — PITR 저장량과 데이터 크기의 1:1
PITR 을 계속 켜 둘지는 비용으로 판단해야 했다. 처음 조사의 추정은 저장 비용이 거의 들지 않는다는 쪽이었는데, 7일 창이 가득 차기 전에 같은 방법으로 미리 재 보니 크게 빗나갔다.
| Cloud Monitoring 메트릭 | 실측 |
|---|---|
firestore.googleapis.com/storage/data_and_index_storage_bytes | 142.25 GiB |
firestore.googleapis.com/storage/pitr_storage_bytes | 142.25 GiB |
firestore.googleapis.com/storage/backups_storage_bytes | 시계열 0개 |
PITR 저장량이 데이터 크기와 바이트 단위까지 같았다. 같은 값이라 필터가 안 먹어 같은 시계열을 두 번 받은 것이 아닌지 의심했지만, 응답의 metric.type 이 각자 제 이름으로 찍혀 서로 다른 시계열이었다. 존재하지 않는 메트릭 이름으로 한 번 더 부르면 404 가 나는 것도 확인했다. 빈 결과와 조회 실패를 가르는 방법은 이 대조군뿐이다. Billing Catalog 의 단가로 보면 PITR 단가가 저장 단가와 같으므로, PITR 을 켜면 Firestore 저장 비용이 정확히 두 배가 된다. 처음 추정은 5~25배 빗나간 셈이다.
선택지도 생각보다 좁았다. gcloud firestore databases update 에는 --enable-pitr 와 --no-enable-pitr 둘뿐이고 보존 기간을 지정하는 인자가 없다. 켜면 7일 고정, 끄면 1시간이다. 「창을 줄여 비용을 아낀다」는 길은 없다. 그래서 판단은 7일 창이 가득 차는 날 같은 명령으로 다시 잰 값과, 끄면 잃는 복구 범위를 나란히 놓고 하기로 했다.
결과 — 지킨 복구 지점과 남은 비용 판단
- 복구 지점. Firestore 는 삭제 보호·PITR 7일·매일 백업 3일 보존이 생겼고, Realtime Database 는 집행 순서를 바꿔 하나뿐인 복구 지점을 지켰다
- 비용 판단의 근거. PITR 이 저장 비용을 정확히 두 배로 만든다는 실측으로, 켜 둘지를 감이 아니라 복구 범위와 비용을 나란히 놓고 정하게 됐다
- 남긴 일. Realtime Database 백업 켜기는 콘솔 전용이라 담당자의 손이 필요하고, 수명 정책은 폴더 지정 방식이 되는지 확인한 뒤에 건다
정리
- 백업이 없으면 먼저 대조군을 찾는다. 같은 조직에서 백업이 도는 것이 있으면, 문제는 정책이 아니라 누락이다
- 수명 정책은 기존 객체에도 걸린다. 백업이 귀한 버킷에 lifecycle 을 먼저 걸면 마지막 복구 지점이 그 자리에서 지워진다
- 설정 변경은 명령의 성공이 아니라 재조회로 기록한다. 시험하지 않은 것은 시험하지 않았다고 적는다
- 비용 추정은 같은 창 크기에서 미리 재 본다. 메트릭이 0 이거나 같은 값이면 대조군으로 조회가 닿았는지부터 확인한다