Velero 백업, 같은 클러스터에 저장하면 DR이 아니다
Velero 백업이 같은 클러스터의 MinIO에 저장되어 있었다. 클러스터가 죽으면 백업도 같이 사라진다. OCI에 Garage를 배포하고 CronJob으로 DR 복제를 구성한 뒤, DB Hook으로 백업 데이터의 일관성까지 확보한 과정.
Velero 백업 시스템을 구축하고 FSB를 최적화했다. 백업이 안정화된 지금, 처음부터 염두에 두었던 문제를 해결할 차례다. 백업 저장소인 MinIO가 같은 클러스터에서 동작하고 있다.
클러스터가 죽으면? 백업도 같이 사라진다. 이건 DR(재해 복구)이 아니다.
문제: 단일 장애점
현재 백업 아키텍처는 이렇다:
flowchart LR V[Velero] -->|백업 저장| M[MinIO] subgraph home[Home Cluster] V M endVelero가 백업한 데이터가 같은 클러스터의 MinIO에 저장된다. 노드 장애, 디스크 장애, 실수로 namespace를 삭제하는 경우 — 모두 백업이 살아있어야 복구할 수 있는데, 백업이 같은 장애 도메인에 있다.
“NAS로 복제하면 되지 않나?” 가능하지만, 같은 물리 환경이다. 화재나 정전이면 NAS도 같이 죽는다.
지리적으로 분리된 원격 스토리지가 필요했고, 마침 OCI Always Free tier로 운영 중인 Kubernetes 클러스터가 있었다.
해결: OCI에 Garage 배포 + CronJob 동기화
목표 아키텍처:
flowchart LR V[Velero] -->|백업 저장| M[MinIO] M -->|CronJob<br>매일 동기화| G[Garage] subgraph home[Home Cluster] V M end subgraph oci[OCI Cluster — Always Free] G endOCI에 왜 Garage인가
OCI Always Free 클러스터에 S3 호환 스토리지를 배포해야 한다. 선택지는 MinIO와 Garage 두 가지였다.
| 비교 | MinIO | Garage |
|---|---|---|
| 리소스 요구 | 메모리 512MB~1GB+ | 메모리 128MB~256MB |
| 분산 모드 | 최소 4 drive 필요 | 단일 노드 가능 |
| 멀티사이트 | 별도 설정 | 내장 기능 |
| ARM 지원 | 공식 지원 | 공식 지원 |
OCI Always Free는 ARM 인스턴스 2대(각 2 OCPU / 12GB)인데, 이미 n8n 같은 서비스가 돌아가고 있다. MinIO의 메모리 사용량이 부담스러웠고, 향후 홈클러스터와 멀티사이트 복제로 고도화할 계획이 있어서 Garage를 선택했다.
Garage 배포
# OCI Garage 핵심 설정garage: replicationFactor: "1" # OCI 단일 노드image: repository: dxflrs/arm64_garage # ARM 이미지 tag: "v2.2.0"persistence: meta: storageClass: oci-bv size: 1Gi data: storageClass: oci-bv size: 50Gi배포 후 버킷과 API 키를 생성하고, HTTPRoute로 HTTPS 엔드포인트(garage.heeho.net)를 노출했다. S3 API 자체가 AWS Signature V4 인증을 사용하므로 별도 mTLS 없이도 충분하고, Rate Limiting(100 req/s)만 추가했다.
동기화 도구: mc mirror vs rclone
Home MinIO의 Velero 버킷을 통째로 OCI Garage에 동기화한다. 선별 복제도 고려했지만, Garage 디스크(50GB)에 충분히 들어가는 크기(~13GB)라 관리 포인트만 늘릴 뿐이었다. 버킷 전체를 복제하기로 했다.
mc mirror
MinIO 공식 클라이언트인 mc의 mirror 명령이 가장 자연스러운 선택이다. 소스가 MinIO이므로 호환성 문제도 없다.
mc mirror --overwrite home/velero oci/velero-backup한 가지 알아둘 점이 있다. mc mirror는 동기화 전에 전체 오브젝트 목록을 메모리에 로드한다. 오브젝트 수가 적으면 문제없지만, Kopia는 데이터를 작은 chunk로 분할 저장하는 방식이라 오브젝트 수가 빠르게 늘어난다.
이 환경의 Velero 버킷은 ~4,300개 오브젝트로, FSB 최적화 후 정상 상태 기준 ~13GB 규모다. mc mirror가 동작하지 못할 크기는 아니지만, 백업 대상이 늘어나면 오브젝트 수도 함께 늘어난다.
rclone
rclone은 범용 클라우드 스토리지 동기화 도구다. GitHub 40k+ stars로 레퍼런스가 충분하고, S3 호환 스토리지 간 동기화에도 많이 사용된다.
mc mirror와 다르게 오브젝트 목록을 스트리밍 방식으로 처리하기 때문에, 오브젝트 수가 늘어나도 메모리 사용량이 일정하다.
rclone sync home:velero oci:velero-backup선택 기준
| 기준 | mc mirror | rclone |
|---|---|---|
| MinIO 호환 | 공식 클라이언트 | 범용 (S3 호환) |
| 메모리 사용 | 오브젝트 수에 비례 | 일정 |
| 설정 복잡도 | 간단 (mc alias) | rclone.conf 필요 |
| 레퍼런스 | MinIO 생태계 | 범용 (GitHub 40k+) |
소스가 MinIO이고 오브젝트 수가 통제 가능한 범위라면 mc mirror가 자연스럽다. 오브젝트 수가 수천~수만으로 늘어날 가능성이 있거나, 메모리 제한이 타이트한 환경이라면 rclone이 안전하다.
어느 쪽을 선택해도 동작하지만, rclone의 예측 가능한 메모리 사용이 리소스 제한이 있는 Always Free 환경에 더 적합하다고 판단하여 rclone을 선택했다.
CronJob 구현
Velero의 일일 백업(daily-backup)이 03:00 KST에 실행되므로, 동기화는 03:30에 실행한다.
apiVersion: batch/v1kind: CronJobmetadata: name: velero-backup-mirror namespace: velerospec: schedule: "30 18 * * *" concurrencyPolicy: Forbid jobTemplate: spec: backoffLimit: 2 template: spec: containers: - name: rclone image: rclone/rclone:latest command: - /bin/sh - -c - | set -e mkdir -p /tmp/rclone cat > /tmp/rclone/rclone.conf << EOF [home] type = s3 provider = Minio endpoint = http://velero-minio.velero.svc:9000 access_key_id = ${HOME_USER} secret_access_key = ${HOME_PASSWORD}
[airflow] type = s3 provider = Minio endpoint = http://airflow-minio.airflow.svc:9000 access_key_id = ${AIRFLOW_MINIO_USER} secret_access_key = ${AIRFLOW_MINIO_PASSWORD} region = us-east-1
[oci] type = s3 provider = Other endpoint = ${OCI_ENDPOINT} access_key_id = ${OCI_ACCESS_KEY} secret_access_key = ${OCI_SECRET_KEY} region = garage force_path_style = true EOF
CONF="/tmp/rclone/rclone.conf" OPTS="--config $CONF --transfers 4 --checkers 4 --stats-one-line -v"
echo "--- Velero backup sync ---" rclone sync home:velero oci:velero-backup $OPTS
echo "--- Airflow CNPG backup sync ---" rclone sync airflow:postgres oci:airflow-pg-backup $OPTS env: - name: HOME_USER valueFrom: secretKeyRef: name: velero-minio-credentials key: rootUser # HOME_PASSWORD, OCI_ENDPOINT, OCI_ACCESS_KEY, OCI_SECRET_KEY 동일 패턴 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi restartPolicy: OnFailure몇 가지 포인트:
- rclone.conf를 런타임에 생성한다. Secret에서 환경변수로 주입받은 인증 정보로 설정 파일을 만든다. ConfigMap에 넣으면 인증 정보가 노출되고, Secret을 파일로 마운트하면 rclone.conf 포맷과 맞지 않는다
homeremote는 Velero MinIO를 가리킨다.velero-minio.velero.svc:9000airflowremote는 Airflow MinIO를 가리킨다. CNPG가 WAL을 저장하는 별도 MinIO다ociremote는 외부 엔드포인트를 사용한다.garage.heeho.net은 HTTPS로 노출되어 있다--checkers 4로 병렬 체크를 줄였다. OCI Garage의 Rate Limit(100 req/s)에 걸리지 않도록 조정
Velero 버킷 구조
동기화되는 버킷의 내부 구조는 이렇다:
| 경로 | 내용 |
|---|---|
backups/ | K8s 리소스 정의(Deployment, Service, Secret 등) + 백업 메타 정보 |
kopia/<namespace>/ | namespace별 FSB 데이터 (PVC 내용물) |
Velero 복원은 backups/에서 리소스 정의를 읽어 Deployment, Service 등을 재생성하고, kopia/에서 파일 데이터를 읽어 PVC에 채운다. 둘 중 하나만 있으면 복원이 안 된다.
결과
flowchart LR V[Velero] -->|03:00<br>일일 백업| M[Velero MinIO] A[Airflow CNPG] -->|WAL 아카이브| AM[Airflow MinIO] M -->|03:30<br>rclone sync| G[Garage] AM -->|03:30<br>rclone sync| G subgraph home[Home Cluster] V M A AM end subgraph oci[OCI Cluster] G endVelero 백업과 Airflow CNPG WAL이 매일 OCI Garage에 복제된다. 클러스터가 완전히 죽더라도 OCI에서 서비스를 복원할 수 있다.
복제해도 데이터가 깨져있으면
원격 복제로 “어디에 저장할 것인가”는 해결됐다. 하지만 한 가지 더 신경 써야 할 문제가 있다.
FSB(File-System Backup)는 Pod에 마운트된 볼륨의 파일을 그대로 복사한다. 일반 파일이라면 문제없지만, PostgreSQL처럼 데이터 디렉토리에 WAL과 데이터 파일이 함께 있는 경우 — 백업 중에 쓰기가 발생하면 파일 간 불일치가 생길 수 있다.
Velero Backup Hook
Velero는 백업 전후에 컨테이너 안에서 명령을 실행하는 Backup Hook을 지원한다. Pod annotation으로 선언한다:
# 백업 전: pg_dump로 일관된 덤프 생성pre.hook.backup.velero.io/command: >- ["/bin/sh", "-c", "PGPASSWORD=$POSTGRES_PASSWORD pg_dump -U gitlab -d gitlabhq_production -Fc -f /bitnami/postgresql/data/velero-backup.dump"]pre.hook.backup.velero.io/container: postgresql# 백업 후: 덤프 파일 삭제post.hook.backup.velero.io/command: >- ["/bin/sh", "-c", "rm -f /bitnami/postgresql/data/velero-backup.dump"]post.hook.backup.velero.io/container: postgresql동작 순서:
pre-hook: pg_dump 실행 → velero-backup.dump 생성 ↓FSB: data 디렉토리 전체 백업 (dump 파일 포함) ↓post-hook: dump 파일 삭제pg_dump는 트랜잭션 일관성이 보장된 덤프를 생성한다. FSB가 데이터 디렉토리를 복사하는 동안 데이터 파일이 불일치하더라도, dump 파일만 있으면 정상 복원이 가능하다.
-Fc(custom format)는 pg_restore로 복원할 수 있는 압축 바이너리 형식이다. SQL 텍스트보다 작고, 테이블 단위 선택 복원도 가능하다.
적용 대상
Helm values의 podAnnotations에 hook을 선언하면 Pod 재생성 시 자동 적용된다:
postgresql: primary: podAnnotations: backup.velero.io/backup-volumes: "data" pre.hook.backup.velero.io/command: >- ["/bin/sh", "-c", "PGPASSWORD=$POSTGRES_PASSWORD pg_dump ..."] # ...| 서비스 | DB | dump 크기 |
|---|---|---|
| GitLab | gitlabhq_production | ~330MB |
| Langfuse | postgres_langfuse | ~50MB |
| SonarQube | sonarDB | ~20MB |
| DefectDojo | defectdojo | ~7MB |
정리
- 백업과 원본이 같은 장애 도메인에 있으면 DR이 아니다 — 지리적으로 분리된 원격 스토리지 필요
- 버킷 통째로 복제한다 — 선별 복제는 관리 포인트만 늘린다. 용량이 허용되면 전체 복제가 단순하고 확실하다
- 동기화 도구 선택 — 소스가 MinIO면 mc mirror가 자연스럽고, 오브젝트 수 증가가 예상되면 rclone이 안전하다
- FSB로 DB를 백업하면 hook이 필요하다 — pg_dump pre-hook으로 일관된 덤프를 만들어야 복원 시 쓸 수 있다
- CNPG WAL 아카이브도 같이 복제한다 — Airflow PostgreSQL은 CNPG로 운영되며 WAL을 별도 MinIO에 저장한다. Velero 백업과 함께 DR 복제해야 point-in-time recovery가 가능하다
참고 자료
관련 콘텐츠
Velero로 Kubernetes 백업 시스템 구축하기
Homelab K8s 클러스터에 Velero + MinIO 백업 시스템을 구축하고, CSI 스냅샷 실패를 FSB로 해결한 뒤 복원까지 검증한 과정
KubernetesVelero FSB 백업이 4시간 걸린다고?
defaultVolumesToFsBackup: true의 함정과 opt-in 방식으로 백업 시간 99% 단축하기
KubernetesGateway API, Ingress를 대체하는 Kubernetes 표준
SIG-Network이 4년에 걸쳐 만든 Gateway API의 핵심 리소스 3가지와 그 관계를 이해하고, Ingress와 무엇이 달라졌는지 정리한다.