Published on

Kubernetes의 DB는 누가 백업할까: Day To Us의 일일 백업과 복원 검증

Authors

“지금 k8s로만 띄워져 있을 텐데, 백업할 수 있을까?”

Day To Us는 커플이 가계부와 일정을 공유하는 PWA다. 기능을 추가하고 배포하는 동안 운영 데이터도 쌓였다. 그러다 DB를 실행하고 있다는 사실과 데이터를 되돌릴 수 있다는 사실이 별개라는 질문에 도달했다.

서버의 DB 주기적으로 백업할 수 있을까? 지금 k8s 로만 띄워져있을텐데

이 질문으로 AI에게 현재 운영 구성을 확인하고 정기 백업을 구성하도록 요청했다. 외부 저장소 선택에 대해서는 “우선 서버 내 별도 저장소에 백업”이라고 답했다. 그 선택에 따라 매일 한국 시간 새벽 3시에 DB를 백업하고 최근 30일치를 별도 볼륨에 보관하는 구성을 적용했다.

이 글은 그 작업의 코드와 실행 결과를 다시 확인한 기록이다. 명령 실행과 복원 검증은 AI가 운영 환경에서 수행했고, 아래 결과는 당시 대화의 출력 및 초안 작성 시 재조회한 상태를 근거로 한다. 내가 직접 장애 복구를 수행했다거나 모든 복구 시나리오를 검증했다는 뜻은 아니다.

화면 자료는 구조 설명용 그림과 실제 CLI 출력을 읽기 좋게 재구성한 이미지다. 원본 터미널 스크린샷으로 오해하지 않도록 캡션을 구분했다. 같은 내용을 복사할 수 있는 코드 블록도 함께 넣었다.


1. PVC가 있으면 데이터는 남는다. 과거 데이터도 남을까?

확인 당시 DB는 postgres:16-alpine 이미지로 실행 중이었다. 데이터 디렉터리에는 postgres-pvc가 연결돼 있었고, StorageClass는 local-path였다. 정기 백업 CronJob은 없었다.

백업 적용 전 상태를 확인한 명령이다. 이후에 실행하면 이번에 추가한 리소스도 조회된다.

kubectl -n day-to-us get deployment day-to-us-db
kubectl -n day-to-us get cronjobs,pvc
kubectl -n day-to-us get deployment day-to-us-db \
  -o jsonpath='{.spec.template.spec.containers[*].image}{"\n"}'

PVC는 Pod 생명주기와 저장 공간을 분리한다. 하지만 사용자가 잘못 삭제한 행이나 애플리케이션이 잘못 수정한 값도 현재 DB에 그대로 저장된다. 저장 공간이 유지되는 것과 이전 시점의 사본이 있는 것은 다른 조건이다.

이번 구성은 운영 DB의 파일을 그대로 복사하는 대신, pg_dump로 복원 가능한 논리 백업 파일을 만든다.

운영 DB PVC와 별도 백업 PVC는 같은 홈 서버 디스크에 있지만 서로 다른 역할을 가진다

구조 설명 그림. 별도 PVC는 별도 물리 디스크나 다른 서버를 뜻하지 않는다.

구분운영 DB새 백업 저장소
PVCpostgres-pvcpostgres-backup-pvc
요청 용량5Gi1Gi
StorageClasslocal-pathlocal-path
내용실행 중인 PostgreSQL 데이터 파일시점별 .dump 파일
용도앱의 조회·수정과거 백업을 이용한 복원

실제 홈 서버에서 확인한 구성

2026년 10월 15일 운영 서버를 읽기 전용으로 조회했다. IP, 접속 도메인, 노드 이름, 볼륨 식별자와 인증 값은 공개하지 않고 백업 구조를 이해하는 데 필요한 정보만 남긴다.

항목실제 확인 값
운영체제Ubuntu 24.04.4 LTS
CPU 아키텍처x86_64 (linux/amd64)
Kubernetes 서버v1.34.3
컨테이너 런타임containerd 2.3.3
DB·백업 이미지postgres:16-alpine
저장 방식local-path, 두 PVC 모두 Bound, 접근 모드 RWO
루트 파일시스템468G 중 39G 사용, 405G 가용, 사용률 9%

디스크 수치는 df -h /의 당시 출력이다. 실제 파일시스템의 가용 공간이며, 백업 PVC의 요청 용량인 1Gi와 다른 값이다. 5Gi와 1Gi짜리 독립된 물리 디스크를 확보한 것으로 해석하면 안 된다. 또한 전체 여유 공간에는 다른 서비스의 사용량도 영향을 준다.

노드 이름이나 IP가 출력되지 않도록 필요한 필드만 골라 확인할 수 있다.

kubectl get nodes -o jsonpath='{range .items[*]}{.status.nodeInfo.osImage}{" | "}{.status.nodeInfo.containerRuntimeVersion}{"\n"}{end}'
kubectl version -o jsonpath='{.serverVersion.gitVersion}{"\n"}'
kubectl -n day-to-us get pvc postgres-pvc postgres-backup-pvc \
  -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,CAPACITY:.status.capacity.storage,CLASS:.spec.storageClassName
# 아래 명령은 서버 셸에서 실행한다.
df -h /

현재 서버에서 DB 크기는 약 8MB였고 첫 압축 백업은 37,885 bytes였다. 두 값은 물리 DB 크기와 논리 백업 크기이므로 같은 크기일 필요가 없다. 지금 작은 백업이 만들어졌다는 사실만으로 앞으로의 저장 공간이 충분하다고 보장할 수는 없다.


2. CronJob은 언제, Job은 무엇을 실행하는가?

실제 설정은 백엔드 저장소의 k8s/base/db-backup.yaml에 있다. k8s/base/kustomization.yaml의 resources에도 이 파일을 추가했다. 전체 변경은 백업 구성 PR #17에서 확인할 수 있다.

CronJob이 새벽 3시에 Job과 Pod를 만들고 DB를 읽어 백업 파일을 저장하는 흐름

아래는 실제 CronJob 설정에서 실행 정책만 발췌한 부분이다.

k8s/base/db-backup.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
  namespace: day-to-us
spec:
  schedule: "0 3 * * *"
  timeZone: Asia/Seoul
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 3600
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 600
설정이 구성에서의 의미
schedule + timeZone한국 시간 매일 03:00에 실행을 예약한다.
concurrencyPolicy: Forbid같은 CronJob의 이전 작업이 실행 중이면 겹쳐 실행하지 않는다.
startingDeadlineSeconds: 3600놓친 예정 시각으로부터 한 시간 안의 실행만 따라잡을 수 있게 한다.
backoffLimit: 2실패한 Job의 재시도 한도를 정한다.
activeDeadlineSeconds: 600재시도를 포함한 Job 실행 시간이 10분을 넘지 않도록 한다.
성공·실패 이력 각각 3개Kubernetes에 남길 Job 이력 수다. 백업 파일의 보관 일수와는 별개다.

CronJob은 지정 시각에 정확히 한 번만 실행되는 보장을 제공하지 않는다. Forbid도 이 CronJob에서 생성한 작업에 대한 정책이며, 사람이 별도로 만든 수동 Job까지 전역 잠금으로 막아주는 것은 아니다. 그래서 파일명에도 시각과 Pod 이름을 함께 넣어 서로 다른 실행이 같은 이름을 덮어쓰는 일을 피했다. Kubernetes CronJob 문서

파일명의 시각은 UTC다. 첫 백업의 20261014T145755Z는 한국 시간으로 2026년 10월 14일 23:57:55다. 스케줄은 한국 시간, 파일명은 UTC라는 차이를 기록해 두면 새벽에 복구할 때 덜 헷갈린다.


3. 백업 Pod가 DB에 접속하는 방법

백업 Pod는 같은 네임스페이스의 Service 이름 db로 PostgreSQL에 접속한다. 비밀번호를 YAML 본문이나 Git에 직접 넣지 않고 기존 Kubernetes Secret을 참조한다.

k8s/base/db-backup.yaml
env:
  - name: PGHOST
    value: db
  - name: PGDATABASE
    value: daytous
  - name: PGCONNECT_TIMEOUT
    value: "15"
  - name: PGUSER
    valueFrom:
      secretKeyRef:
        name: day-to-us-secret
        key: POSTGRES_USER
  - name: PGPASSWORD
    valueFrom:
      secretKeyRef:
        name: day-to-us-secret
        key: POSTGRES_PASSWORD

현재 DB의 기존 사용자 자격 증명을 재사용한다. 백업 전용 최소 권한 계정을 새로 만든 구현은 아니다. Kubernetes API를 호출할 필요도 없어서 automountServiceAccountToken: false를 지정했다.

PostgreSQL 16 Alpine 이미지에서 확인한 postgres 사용자의 UID/GID는 70이다. 백업 Pod도 UID/GID 70으로 실행하고, 볼륨 쓰기 권한에는 fsGroup: 70을 사용한다. 이 숫자를 모든 PostgreSQL 이미지의 공통값으로 가정하면 안 된다.

kubectl -n day-to-us exec deployment/day-to-us-db -- id postgres

실제 출력이다.

uid=70(postgres) gid=70(postgres) groups=70(postgres),70(postgres)

루트 파일시스템은 읽기 전용으로 두고 /backup 볼륨에만 백업 파일을 쓴다. CPU 한도는 500m, 메모리 한도는 256Mi로 설정했다. 현재 데이터 규모에 맞춘 시작값이며, DB가 커지면 백업 소요 시간과 함께 재검토해야 한다.


4. “덤프 명령이 끝났다”를 완료 기준으로 삼기까지

다음은 실제 CronJob 안에서 실행하는 셸 스크립트다. /bin/sh -ec로 실행하므로 중간 명령이 실패하면 뒤의 완료 처리로 진행하지 않는다.

k8s/base/db-backup.yaml
umask 077
stamp=$(date -u +%Y%m%dT%H%M%SZ)
archive="/backup/daytous-${stamp}-${HOSTNAME}.dump"
partial="${archive}.partial"
trap 'rm -f "$partial"' EXIT
pg_dump --format=custom --no-owner --no-privileges --lock-wait-timeout=60s --file="$partial"
pg_restore --list "$partial" >/dev/null
mv "$partial" "$archive"
# Only expire completed archives after a new backup succeeds.
find /backup -maxdepth 1 -type f -name 'daytous-*.dump' -mtime +29 -delete
echo "Backup completed: $(basename "$archive") ($(wc -c < "$archive") bytes)"

읽는 순서는 다음과 같다.

  1. umask 077로 새 파일에 다른 사용자의 접근 권한이 생기지 않도록 한다.
  2. 아직 끝나지 않은 결과는 .partial 이름으로 쓴다.
  3. pg_dump가 성공하면 pg_restore --list로 아카이브 목록을 읽을 수 있는지 확인한다.
  4. 같은 백업 볼륨 안에서 완료 파일명 .dump로 바꾼다.
  5. 새 백업을 확보한 다음 오래된 완료 파일을 정리한다.

--format=custom은 pg_restore가 읽을 수 있는 압축 아카이브 형식이다. --list는 아카이브의 목록을 읽는 검사이며, 모든 데이터가 실제 DB에 복원된다는 보장까지 제공하지는 않는다. 그래서 이번에는 별도의 복원 실행도 검증했다. PostgreSQL 16 pg_dump 문서, pg_restore 문서

운영 중 변경되는 데이터는 어느 시점을 읽는가?

PostgreSQL 16의 일반적인 pg_dump는 자체 읽기 전용 트랜잭션을 열고 REPEATABLE READ 스냅샷으로 일관된 데이터를 읽는다. 이번 명령에는 --serializable-deferrable이나 병렬 덤프 옵션을 지정하지 않았다. 앱의 가계부 수정 트랜잭션과는 별도이며, 앱의 격리 수준을 바꾼 작업도 아니다. PostgreSQL 16 pg_dump 구현

따라서 백업 도중 새로 커밋된 변경이 마지막 테이블에만 임의로 섞이는 방식으로 이해하면 안 된다. 반대로 백업의 스냅샷 이후 변경까지 모두 포함되는 것도 아니다. 읽기·쓰기 서비스가 동작하는 동안 백업할 수 있지만, 테이블 구조 변경과의 잠금 충돌이나 I/O 부하는 별도로 고려해야 한다.

--lock-wait-timeout=60s는 시작 단계에서 테이블 잠금을 기다리는 시간을 제한한다. 백업 전체의 실행 제한은 앞의 Job 설정인 600초다.


5. “최근 30일”은 파일 30개라는 뜻이 아니다

보관 정책은 파일의 수정 시각을 기준으로 한다.

find /backup -maxdepth 1 -type f \
  -name 'daytous-*.dump' -mtime +29 -delete

-mtime은 24시간 단위로 계산한다. 이 조건은 30일 이상 지난 완료 파일을 삭제 대상으로 삼는다. 같은 날 수동 백업을 두 번 실행하면 파일이 두 개 생길 수 있으므로, 파일 개수를 30개로 맞추는 정책과 다르다.

아래 표는 실제 장애가 아니라 순서를 이해하기 위한 가정한 시나리오다.

상황현재 스크립트의 결과
새 덤프 생성 실패완료 이름으로 바꾸지 않고, 오래된 백업 삭제까지 진행하지 않는다.
아카이브 목록 검사 실패완료 처리와 오래된 백업 삭제를 수행하지 않는다.
새 백업 성공완료 파일로 확정한 뒤 30일 이상 된 완료 파일을 정리한다.
노드가 강제로 꺼짐종료 처리 자체가 실행되지 않아 .partial이 남을 수 있다. 완료 백업으로 선택되지는 않는다.
정리 명령 실패새 완료 파일은 존재할 수 있지만 Job은 실패할 수 있으므로 로그를 확인한다.

정상 종료 시의 trap이 전원 차단이나 강제 종료까지 처리해주는 것은 아니다. 또 local-path에 요청한 1Gi를 실제 디스크 사용량의 강제 상한으로 가정하지 않는다. 공간이 부족해 새 백업에 실패하면 오래된 파일 정리도 실행되지 않으므로 디스크 점검이 필요하다.

PVC에는 다음 Argo CD 옵션도 설정했다.

annotations:
  argocd.argoproj.io/sync-options: Prune=false,Delete=false

동기화 과정의 자동 prune과 애플리케이션 삭제에 따른 백업 PVC 삭제를 막기 위한 설정이다. 관리자의 직접 삭제, PV reclaim policy, 물리 디스크 손상까지 해결하는 옵션은 아니다. Argo CD Sync Options


6. 첫 백업을 바로 실행하고 상태 확인하기

아래 명령은 서버에서 적용한 매니페스트의 검증·실행 순서를 재현하는 예시다. kubectl이 운영 클러스터를 가리키는 환경에서 백엔드 저장소 루트를 기준으로 실행한다. apply와 create job은 실제 리소스를 생성하므로 상태 조회 명령과 구분해야 한다.

# 서버의 Kubernetes 스키마로 검증만 한다.
kubectl apply --dry-run=server -f k8s/base/db-backup.yaml

# 정기 백업 설정을 적용한다.
kubectl apply -f k8s/base/db-backup.yaml

# 새벽까지 기다리지 않고 첫 백업을 생성한다.
backup_job="postgres-backup-manual-$(date +%s)"
kubectl -n day-to-us create job \
  --from=cronjob/postgres-backup "$backup_job"
kubectl -n day-to-us wait --for=condition=complete \
  "job/$backup_job" --timeout=120s
kubectl -n day-to-us logs "job/$backup_job"

실제 최초 Job의 이름은 postgres-backup-initial-20261014였다. 위 예시에서는 다시 실행할 때 이름이 충돌하지 않도록 시각을 사용한다. wait의 120초는 CLI가 기다리는 시간이며, 백업 Job 자체의 600초 제한을 바꾸지는 않는다.

실제 백업 활성 상태와 분리된 PVC 조회 결과를 재구성한 CLI 이미지

실제 조회 출력 재구성. 원본 화면 캡처가 아니다. 이미지의 열 이름은 설명을 위해 축약했다.

첫 정기 실행 전, 초안을 작성하면서 확인했던 결과다. 아래 이미지는 이 최초 확인 시점의 기록으로 유지한다.

NAME              SCHEDULE    TIMEZONE     SUSPEND   LAST
postgres-backup   0 3 * * *   Asia/Seoul   false     <none>

NAME                  STATUS   CAPACITY   CLASS
postgres-pvc          Bound    5Gi        local-path
postgres-backup-pvc   Bound    1Gi        local-path

LAST가 <none>인 것은 이 최초 조회 시점에 정기 일정으로 실행된 이력이 아직 없었기 때문이다. 당시 수동 Job 성공과 이후 정기 실행 성공은 서로 다른 검증 단계다.

Backup completed: daytous-20261014T145755Z-postgres-backup-initial-20261014-9d7fp.dump (37885 bytes)

다음 날 새벽 3시에는 실제로 실행됐을까?

이후 “새벽 3시에 백업 잘 되었는지 확인해”라고 요청했다. 2026년 10월 15일 다시 조회하니, 수동 Job과 별개로 postgres-backup-29866680이 생성되어 성공한 상태였다.

확인 항목운영 서버에서 확인한 결과
Job 소유자CronJob postgres-backup
CronJob 마지막 예약 시각2026-10-14T18:00:00Z → 한국 시간 10월 15일 03:00:00
CronJob 마지막 성공 시각2026-10-14T18:00:04Z → 한국 시간 10월 15일 03:00:04
Job 상태Complete=True, 성공 Pod 1개
컨테이너 종료종료 코드 0, 재시작 0회
생성된 아카이브37,880 bytes

Job 시작부터 완료까지는 상태 시각 기준 4초다. pg_dump 명령만 별도로 측정한 실행 시간은 아니다. 현재 데이터가 작을 때의 한 번의 결과이므로 데이터가 커진 뒤에도 4초 안에 끝난다는 의미는 아니다.

확인에는 다음 조회 명령을 사용했다. 이 명령은 새 백업을 만들지 않는다.

kubectl -n day-to-us get cronjob postgres-backup \
  -o jsonpath='{.status.lastScheduleTime}{"\n"}{.status.lastSuccessfulTime}{"\n"}'
kubectl -n day-to-us get job postgres-backup-29866680 \
  -o jsonpath='{.metadata.ownerReferences[0].kind}{" "}{.metadata.ownerReferences[0].name}{"\n"}{.status.conditions}{"\n"}'
kubectl -n day-to-us logs job/postgres-backup-29866680
kubectl -n day-to-us get pods -l job-name=postgres-backup-29866680 \
  -o jsonpath='{range .items[*]}{.status.phase}{" exit="}{.status.containerStatuses[0].state.terminated.exitCode}{" restart="}{.status.containerStatuses[0].restartCount}{"\n"}{end}'

실제 로그와 종료 상태는 다음과 같다.

Backup completed: daytous-20261014T180000Z-postgres-backup-29866680-nh8rc.dump (37880 bytes)
Succeeded exit=0 restart=0

이름만 보고 자동 실행이라고 판단하지 않고 Job의 ownerReferences가 해당 CronJob을 가리키는지도 확인했다. 마지막 예약·성공 시각, Job 완료 상태, 프로세스 종료 코드, 아카이브 완료 로그가 서로 일치했다. 따라서 첫 새벽 3시 정기 백업이 정상 완료된 것까지 확인했다.

이 로그는 덤프 생성뿐 아니라 아카이브 목록 검사, 완료 파일명 변경, 오래된 파일 정리 단계가 성공한 뒤 출력된다. 다만 이번 조회에서 오늘의 파일을 다시 복원하지는 않았다. 아래의 테이블 11개 복원 결과는 전날 최초 백업 파일에 대한 검증이다. 30일 경과 파일의 실제 삭제 동작도 아직 운영 결과로 검증한 것은 아니다.

완료된 Job의 로그는 조회할 수 있지만, 종료된 Pod에 kubectl exec로 새 명령을 실행할 수는 없다. 백업 파일을 읽으려면 백업 PVC를 마운트한 별도 실행 중 Pod가 필요하다. 이번 복원 검증 Job이 그 역할도 한다.


7. 복원 검증은 운영 DB와 분리해서 실행했다

백업이 생성됐다는 것만으로는 충분하지 않다. 저장한 파일로 PostgreSQL이 스키마와 데이터를 실제로 복원할 수 있어야 한다.

이를 위해 k8s/checks/db-restore-check.yaml에 수동 검증 Job을 추가했다. 이 파일은 배포용 Kustomization에 넣지 않았다. 백업 볼륨을 읽기 전용으로 마운트하고, 임시 emptyDir에 PostgreSQL을 새로 띄운다.

읽기 전용 백업 PVC에서 임시 PostgreSQL로 복원하며 운영 DB에는 접속하지 않는 검증 구조

핵심 분리는 다음과 같다.

항목복원 검증 Job
백업 파일/backup, 읽기 전용 마운트
새 DB 파일/scratch/data, 임시 emptyDir
접속 주소/scratch Unix socket, 포트 5433
네트워크 수신listen_addresses=''로 TCP 수신 비활성화
운영 자격 증명·DB PVC검증 Job에 전달하지 않음
정리Job 종료 후 1시간 뒤 TTL 컨트롤러가 Job을 정리하도록 설정

다음은 실제 검증 스크립트 중 DB 시작·복원 부분을 발췌한 것이다. 검증 Pod 안에서 실행되며, 일반 운영 셸에 그대로 붙이는 명령이 아니다.

k8s/checks/db-restore-check.yaml
archive=$(find /backup -maxdepth 1 -type f -name 'daytous-*.dump' | sort | tail -n 1)
test -n "$archive"
initdb -D /scratch/data -U postgres --auth=trust >/dev/null
trap 'pg_ctl -D /scratch/data -m immediate stop >/dev/null 2>&1 || true' EXIT
pg_ctl -D /scratch/data -l /scratch/postgres.log -o "-c listen_addresses='' -k /scratch -p 5433" -w start
createdb -h /scratch -p 5433 -U postgres restore_check
pg_restore -h /scratch -p 5433 -U postgres -d restore_check --exit-on-error --no-owner --no-privileges "$archive"

--auth=trust는 격리된 임시 DB의 로컬 검증용이다. 운영 PostgreSQL 인증 설정을 바꾼 것이 아니다. --no-owner와 --no-privileges는 복원 시 원본 역할·권한을 그대로 재현하는 대신 임시 DB 사용자로 검증하기 위한 옵션이다.

실행할 때는 자동 생성된 Job 이름을 받아서 기다린다.

restore_job=$(kubectl -n day-to-us create \
  -f k8s/checks/db-restore-check.yaml -o name)
kubectl -n day-to-us wait --for=condition=complete \
  "$restore_job" --timeout=120s
kubectl -n day-to-us logs "$restore_job"
실제 백업 및 복원 완료 로그를 재구성한 CLI 이미지

2026년 10월 14일 최초 검증 출력의 핵심 부분을 재구성했다. 화면 폭 때문에 이미지에서는 파일명을 생략했고, 아래에 원문을 남겼다.

 restored_tables
-----------------
              11
(1 row)

Restore verified: daytous-20261014T145755Z-postgres-backup-initial-20261014-9d7fp.dump

restored_tables는 실제 임시 DB의 information_schema.tables를 조회한 값이다. pg_restore --exit-on-error가 성공했고 테이블 11개가 복원됐다. 아카이브 목록을 읽는 것보다 한 단계 더 검증한 결과다.

다만 원본과 복원본의 모든 행을 대조하거나 앱의 로그인·가계부 화면까지 연결한 테스트는 하지 않았다. Alpine 초기화 중 locale 관련 경고도 있었으므로, 운영과 완전히 같은 locale·정렬 동작까지 확인했다고 쓰지는 않는다. 이번 검증의 범위는 저장된 백업을 임시 PostgreSQL에 오류 없이 복원할 수 있는지다.


8. 무엇을 복구할 수 있고, 무엇은 아직 준비되지 않았는가?

상황현재 구성으로 기대할 수 있는 것남은 조건
잘못된 데이터 수정·삭제보관된 백업 시점으로 별도 DB를 복원할 수 있다.필요한 데이터 선택과 서비스 전환 절차가 필요하다.
앱·DB Pod 재생성백업 파일은 별도 PVC에 남는다.PVC와 실제 저장 경로가 유지돼야 한다.
홈 서버 디스크 고장같은 디스크의 백업도 함께 잃을 수 있다.다른 서버·NAS·객체 저장소의 사본이 필요하다.
매일 백업 실패Job 상태와 로그에 실패가 남는다.알림 채널은 아직 연결하지 않았다.
몇 분 전으로 복원이번 일일 논리 백업만으로 보장하지 못한다.WAL 보관과 PITR은 미적용이다.
환경 전체 재구축DB 스키마와 데이터 백업을 사용할 수 있다.DB 역할, Secret, 암호화 키 등은 별도 확보해야 한다.

모든 일일 백업이 성공한다는 조건에서는 다음 백업 직전 사고로 대략 하루치 변경을 잃을 수 있다. 이 범위를 RPO 관점에서 생각할 수 있다. 백업 실패나 장시간 서버 중단이 있으면 더 길어진다. 서비스 전환을 포함한 실제 복구 시간인 RTO는 아직 측정하지 않았다.

Day To Us는 Google OAuth 토큰을 암호화해 저장한다. DB 덤프 안의 암호문만 복원해도 기존 암호화 키가 없으면 토큰을 다시 읽을 수 없다. DB 백업과 Secret·암호화 키 보관은 함께 설계해야 한다. 이 글이나 저장소에 실제 키 값을 넣지는 않는다.

이번 작업에서 답한 것은 “정기적으로 파일을 만들 수 있는가”와 “그 파일로 실제 DB를 만들 수 있는가”다. 외부 사본과 실패 알림, 정기 복원 훈련은 다음 작업으로 남았다. 앞으로는 성공 로그의 존재뿐 아니라 마지막 성공 백업이 얼마나 오래됐는지도 확인해야 한다.

[!NOTE] 첫 정기 실행은 2026년 10월 15일 03:00:04에 성공했다. 이후 외부 저장소와 실패 알림을 연결하면 적용 날짜, 보관 정책, 복원 검증 결과를 추가한다. 정기 백업 파일을 대상으로 복원 훈련을 반복한 기록도 보완할 예정이다.


다시 확인할 때 사용할 CLI

대부분의 점검은 다음 읽기 전용 명령으로 시작할 수 있다.

# 정기 일정과 마지막 성공 시각
kubectl -n day-to-us get cronjob postgres-backup -o yaml

# 최근 실행 결과
kubectl -n day-to-us get jobs --sort-by=.metadata.creationTimestamp

# 생성된 Pod와 저장소 상태
kubectl -n day-to-us get pods
kubectl -n day-to-us get pvc postgres-backup-pvc

# 특정 Job이 실패했다면 실제 이름으로 교체한다.
kubectl -n day-to-us describe job JOB_NAME
kubectl -n day-to-us logs job/JOB_NAME

# 호스트에서 디스크 여유 공간 확인
ssh homeserver 'df -h /'

이 글의 실제 구현과 운영 절차는 db-backup.yaml, 복원 검증 Job, 백업 운영 문서에 남아 있다.

기존 클러스터 구성은 홈 서버 Kubernetes 구성 기록, GitOps 구성 기록은 별도 초안에서 정리 중이다.