Kubernetes Kubernetes Velero MinIO Garage Backup

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
end

Velero가 백업한 데이터가 같은 클러스터의 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
end

OCI에 왜 Garage인가

OCI Always Free 클러스터에 S3 호환 스토리지를 배포해야 한다. 선택지는 MinIO와 Garage 두 가지였다.

비교MinIOGarage
리소스 요구메모리 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이므로 호환성 문제도 없다.

Terminal window
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와 다르게 오브젝트 목록을 스트리밍 방식으로 처리하기 때문에, 오브젝트 수가 늘어나도 메모리 사용량이 일정하다.

Terminal window
rclone sync home:velero oci:velero-backup

선택 기준

기준mc mirrorrclone
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/v1
kind: CronJob
metadata:
name: velero-backup-mirror
namespace: velero
spec:
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 포맷과 맞지 않는다
  • home remote는 Velero MinIO를 가리킨다. velero-minio.velero.svc:9000
  • airflow remote는 Airflow MinIO를 가리킨다. CNPG가 WAL을 저장하는 별도 MinIO다
  • oci remote는 외부 엔드포인트를 사용한다. 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
end

Velero 백업과 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 재생성 시 자동 적용된다:

gitlab.yaml
postgresql:
primary:
podAnnotations:
backup.velero.io/backup-volumes: "data"
pre.hook.backup.velero.io/command: >-
["/bin/sh", "-c", "PGPASSWORD=$POSTGRES_PASSWORD pg_dump ..."]
# ...
서비스DBdump 크기
GitLabgitlabhq_production~330MB
Langfusepostgres_langfuse~50MB
SonarQubesonarDB~20MB
DefectDojodefectdojo~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가 가능하다

참고 자료

관련 콘텐츠

댓글