Kubernetes Kubernetes Velero MinIO Backup Longhorn

Velero로 Kubernetes 백업 시스템 구축하기

Homelab K8s 클러스터에 Velero + MinIO 백업 시스템을 구축하고, CSI 스냅샷 실패를 FSB로 해결한 뒤 복원까지 검증한 과정

들어가며

“etcd 스냅샷 있으니까 괜찮지 않아?”

Homelab 클러스터에 GitLab, Airflow, Grafana 등 다양한 서비스가 운영 중이었지만, 백업 체계가 없었다. etcd 스냅샷만으로는 개별 서비스 복구가 불가능하고, PV 데이터도 보호되지 않는 상태였다.

Velero를 도입하기로 했다. 설치는 순조로웠지만, 첫 백업에서 44개 에러가 발생했다. 원인을 분석하고 해결한 뒤, 실제 복원까지 검증한 과정을 정리한다.


etcd 백업 vs Velero

항목etcd 백업Velero
대상클러스터 전체 상태 (바이너리)선택한 namespace/리소스
PV 데이터포함 안 됨CSI 스냅샷 또는 FSB로 포함
선택적 복구불가 — 전체 복원만namespace, 리소스 단위 복구 가능
복구 시나리오클러스터가 완전히 죽었을 때특정 앱/namespace 복구
복구 방법etcd 재시작 필요 (다운타임)실행 중인 클러스터에 apply

etcd 백업은 “디스크 이미지 통째로 복원”, Velero는 “파일 단위 선택 복원”에 해당한다. 둘은 보완 관계다.


클러스터 환경

Node역할비고
node1Control Plane + Worker일반 워크로드
node2Worker일반 워크로드
node3-gpuWorker + GPUNVIDIA RTX 3060
  • Kubernetes: v1.32.0 (kubespray)
  • CNI: Cilium + Gateway API
  • CSI: Longhorn
  • 시크릿 관리: Sealed Secrets

아키텍처 설계

S3 스토리지: 서비스별 전용 MinIO

클러스터에 이미 여러 MinIO 인스턴스가 서비스별로 돌아가고 있었다:

namespace용도
airflowAirflow 전용
gitlabGitLab 전용 (subchart)
lokiLoki 전용 (subchart)

공용 MinIO를 신규 배포하는 대신, 기존 패턴과 동일하게 Velero 전용 MinIO를 구성했다:

  • 장애 격리 — MinIO 장애 시 영향 범위가 해당 서비스에 한정
  • 관리 일관성 — 기존 서비스와 동일한 패턴
  • 독립적 라이프사이클 — 스토리지 크기, 업그레이드를 서비스별로 관리

컴포넌트 구성

velero namespace
├── velero (Deployment) — 백업 컨트롤러
├── node-agent (DaemonSet x3) — 각 노드에서 FSB 백업
├── velero-minio (Deployment) — S3 호환 스토리지
└── Secrets
├── velero-minio-credentials — MinIO root 인증
└── velero-s3-credentials — Velero S3 접근 인증

Velero의 백업 방식

Velero는 두 가지를 백업한다:

Kubernetes 리소스 — API Server에서 리소스 정의(YAML)를 추출하여 S3에 저장. Deployment, Service, ConfigMap, Secret, CRD 등 모든 리소스를 개별적으로 보관한다.

Persistent Volume 데이터 — 두 가지 방법:

방식동작장점단점
CSI 스냅샷스토리지 드라이버의 스냅샷 기능빠름, point-in-time같은 스토리지 내 저장
File System BackupNodeAgent가 파일을 직접 읽어 S3 전송외부 저장상대적으로 느림

FSB는 내부적으로 kopia를 백업 엔진으로 사용한다. kopia는 데이터를 deduplicated chunk로 분할하고 암호화하여 S3에 저장한다.


설정 및 배포

SealedSecret 생성

하드코딩 없이 Sealed Secrets로 자격증명을 관리한다. 2개의 Secret이 필요하다:

Terminal window
# MinIO root 인증 (rootUser, rootPassword)
kubectl create secret generic velero-minio-credentials -n velero \
--from-literal=rootUser="velero-admin" \
--from-literal=rootPassword="$(openssl rand -base64 32)" \
--dry-run=client -o yaml | \
kubeseal --format yaml | kubectl apply -f -
# Velero S3 인증 (AWS credential 형식)
cat > /tmp/cloud << EOF
[default]
aws_access_key_id=velero-admin
aws_secret_access_key=<same-password>
EOF
kubectl create secret generic velero-s3-credentials -n velero \
--from-file=cloud=/tmp/cloud \
--dry-run=client -o yaml | \
kubeseal --format yaml | kubectl apply -f -

MinIO의 root 인증과 Velero의 S3 인증을 별도 Secret으로 분리한다. 같은 자격증명을 사용하지만 형식이 다르기 때문:

  • velero-minio-credentials: MinIO 서버가 읽는 형식 (rootUser, rootPassword)
  • velero-s3-credentials: Velero가 읽는 AWS credential 형식

MinIO 설정

Helm chart: minio/minio v5.4.0

mode: standalone
existingSecret: velero-minio-credentials
buckets:
- name: velero
policy: none
purge: false
persistence:
enabled: true
storageClass: longhorn
size: 50Gi
resources:
requests:
cpu: 250m
memory: 512Mi

Bitnami 이미지 유료화 이슈

처음에는 Bitnami MinIO chart를 사용하려 했으나, ImagePullBackOff 오류가 발생했다:

Failed to pull image "docker.io/bitnami/minio:2025.7.23-debian-12-r3":
unexpected media type text/html

2025년 8월부터 Bitnami 이미지가 유료화되어 무료 pull이 제한된다. 해결: 공식 minio/minio chart(이미지: quay.io/minio/minio)로 전환.

항목Bitnami chart공식 chart
이미지docker.io/bitnami/minioquay.io/minio/minio
Secret 키root-user, root-passwordrootUser, rootPassword
버킷 생성defaultBuckets: "name"buckets: [{name: ...}]

Velero 설정

Helm chart: vmware-tanzu/velero v11.3.2

initContainers:
- name: velero-plugin-for-aws
image: velero/velero-plugin-for-aws:v1.13.1
volumeMounts:
- mountPath: /target
name: plugins
configuration:
backupStorageLocation:
- name: default
provider: aws
bucket: velero
default: true
config:
region: minio
s3ForcePathStyle: "true"
s3Url: http://velero-minio.velero.svc.cluster.local:9000
volumeSnapshotLocation:
- name: default
provider: csi
defaultVolumeSnapshotLocations: csi:default
credentials:
useSecret: true
existingSecret: velero-s3-credentials
deployNodeAgent: true
snapshotsEnabled: true

배포

Terminal window
# MinIO
helm upgrade --install velero-minio minio/minio \
-f minio.yaml -n velero --version 5.4.0
# Velero
helm upgrade --install velero vmware-tanzu/velero \
-f velero.yaml -n velero --version 11.3.2
# Schedule
kubectl apply -f schedule.yaml

배포 결과

$ kubectl get pods -n velero
NAME READY STATUS NODE
node-agent-8x9x5 1/1 Running node2
node-agent-wwjkd 1/1 Running node1
node-agent-zbntw 1/1 Running node3-gpu
velero-846f566888-6rl2d 1/1 Running node1
velero-minio-b489dbf6b-2wgtj 1/1 Running node1
$ kubectl get backupstoragelocation,schedule -n velero
NAME PHASE DEFAULT
default Available true
NAME STATUS SCHEDULE
daily-backup Enabled 0 18 * * *

첫 백업: CSI 스냅샷 에러

배포 다음 날, 첫 일일 백업 결과를 확인했다.

$ velero backup get
NAME STATUS ERRORS ITEMS
daily-backup-20260202180033 PartiallyFailed 44 5147/5147

5,147개 아이템이 모두 처리되었으나 44개 에러가 발생해 PartiallyFailed 상태.

에러 분석

Velero는 백업 결과를 MinIO에 JSON으로 저장한다. MinIO client(mc)로 다운로드해서 분석했다.

Terminal window
kubectl -n velero port-forward svc/velero-minio 9000:9000 &
mc alias set velero-local http://localhost:9000 $ACCESS_KEY $SECRET_KEY
mc cp velero-local/velero/backups/daily-backup-20260202180033/...-results.gz /tmp/

44개 에러가 전부 동일한 메시지였다:

unable to get valid VolumeSnapshotter for "velero.io/csi"
항목
에러 유형VolumeSnapshotter 없음
대상 리소스persistentvolumes (44개)
StorageClasslonghorn, longhorn-retain, longhorn-1replica

근본 원인

Velero가 각 PV에 대해 CSI 볼륨 스냅샷을 시도했지만, 클러스터에 CSI 스냅샷 인프라가 구성되어 있지 않았다.

Terminal window
$ kubectl get volumesnapshotclass
No resources found
$ kubectl get crd | grep snapshot.storage.k8s.io
# (결과 없음)

CSI 볼륨 스냅샷이 동작하려면 3가지가 필요하다:

컴포넌트역할상태
VolumeSnapshot CRDssnapshot.storage.k8s.io API 정의미설치
snapshot-controllerVolumeSnapshot 요청을 CSI 드라이버에 전달미설치
VolumeSnapshotClass어떤 CSI 드라이버로 스냅샷할지 정의미생성

이 3가지는 Kubernetes 코어에 포함되지 않는 외부 컴포넌트다. 클라우드 매니지드 K8s(GKE 등)에는 기본 포함되기도 하지만, kubespray로 구축한 클러스터에서는 csi_snapshot_controller_enabled: true 설정을 명시적으로 켜야 한다.


해결: CSI → FSB 전환

해결 방안 비교

옵션방법장점단점
ACSI snapshot 인프라 설치point-in-time 스냅샷, 빠름CRD + controller 추가 필요
BsnapshotVolumes: false가장 간단PV 데이터 미백업
CFile-System BackupPV 데이터 실제 백업CSI 스냅샷보다 느림

옵션 C 선택 이유:

  • Velero를 배포할 때 이미 deployNodeAgent: true로 3개 노드 모두에 node-agent를 띄워놓은 상태
  • FSB는 Pod에 마운트된 볼륨의 파일을 직접 읽어 MinIO에 저장하므로, 복원 시 실제 데이터가 복구됨
  • CSI 스냅샷은 나중에 external-snapshotter를 설치한 뒤 전환해도 늦지 않음

설정 변경

# velero.yaml — 변경 전
configuration:
volumeSnapshotLocation:
- name: default
provider: csi
defaultVolumeSnapshotLocations: csi:default
snapshotsEnabled: true
# velero.yaml — 변경 후
configuration:
defaultVolumesToFsBackup: true
defaultBackupStorageLocation: default
snapshotsEnabled: false

핵심 변경:

  • volumeSnapshotLocation, defaultVolumeSnapshotLocations 제거
  • defaultVolumesToFsBackup: true 추가 — 모든 Pod 볼륨을 FSB로 백업
  • snapshotsEnabled: false — CSI 스냅샷 비활성화

결과 비교

항목변경 전 (CSI)변경 후 (FSB)
PhasePartiallyFailedCompleted
Errors440
PodVolumeBackup0130
볼륨 데이터미백업kopia로 백업됨

복원 검증 (PoC)

백업이 정상 동작하는 것을 확인했으니, 실제로 복원이 되는지 검증한다.

테스트 구성

기존 서비스에 영향을 주지 않도록 임시 namespace를 생성하여 배포 → 백업 → 삭제 → 복원 패턴으로 진행했다.

namespace: velero-poc-test
├── Deployment: poc-app (busybox, /data에 파일 기록)
├── PVC: poc-data (1Gi, longhorn-1replica)
├── Service: poc-app (ClusterIP:80)
└── ConfigMap: poc-config (key1, key2, description)

테스트 데이터:

Terminal window
$ kubectl -n velero-poc-test exec deploy/poc-app -- cat /data/test.txt
Velero PoC test data - created at 2026-02-03T11:39:12+09:00
checksum:abc123

백업 → 삭제 → 복원

Terminal window
# 백업
velero backup create poc-backup \
--include-namespaces velero-poc-test \
--default-volumes-to-fs-backup \
--snapshot-volumes=false --ttl 24h --wait
# 결과: Completed (27 items, 에러 0)
# 삭제
kubectl delete namespace velero-poc-test
# 복원
velero restore create poc-restore \
--from-backup poc-backup \
--include-namespaces velero-poc-test --wait
# 결과: Completed (14 items, 에러 0, 경고 2)

복원 검증

리소스복원 상태
NamespaceActive
Pod (poc-app)Running (1/1)
PVC (poc-data)Bound (새 PV 자동 생성)
Service (poc-app)ClusterIP 할당됨
ConfigMap (poc-config)key1/key2/description 정상
PodVolumeRestoreCompleted (kopia, 71 bytes)

경고 2건 (무해):

  • wildcard.heeho.net Secret — reflector가 자동 복제한 것과 충돌하여 skip
  • kube-root-ca.crt ConfigMap — 클러스터가 자동 생성한 것과 충돌하여 skip

복원 동작 방식

Velero의 복원은 다음 순서로 진행된다:

Namespace 생성
→ PVC 생성 (spec.volumeName 제거 → 동적 프로비저닝으로 새 PV 생성)
→ 새 PV ↔ PVC 자동 바인딩
→ ConfigMap, Secret, Service 등 리소스 생성
→ Deployment 생성
→ Pod에 restore-wait init container 자동 주입
→ node-agent가 kopia에서 볼륨 데이터를 새 PV에 복원
→ restore-wait 완료 → 원래 컨테이너 시작

PV/PVC 바인딩 원리

PVC에는 “내가 쓰는 PV 이름”이 spec.volumeName에 기록되어 있다. 하지만 복원 시 원본 PV는 이미 존재하지 않는다. 없는 PV를 그대로 요청하면 PVC가 영원히 Pending 상태에 빠지므로, Velero는 이 필드를 의도적으로 제거한 상태로 PVC를 생성한다. 특정 PV 지정 없이 생성된 PVC를 Kubernetes가 받으면 StorageClass의 동적 프로비저닝이 동작하여 새 PV를 자동으로 생성하고 바인딩한다.

실제 PoC에서 확인한 결과 — PVC 이름(poc-data)은 동일하지만, 바인딩된 PV는 새로 생성되었다:

백업 전: PVC(poc-data) → PV(pvc-0ba69f13-...)
복원 후: PVC(poc-data) → PV(pvc-bd1bfbfb-...) ← 새로 프로비저닝됨

FSB 데이터 복원 원리

새 PV는 비어있는 상태다. Velero는 Deployment를 복원할 때 Pod 스펙에 restore-wait이라는 init container를 자동 주입한다. Pod가 노드에 스케줄링되면 PV가 해당 노드에 마운트되고, restore-wait이 실행되면서 대기 상태에 들어간다. 이때 해당 노드의 node-agent(DaemonSet) 가 호스트에서 PV의 마운트 경로에 직접 접근하여, Kopia 리포지토리에서 백업 데이터를 다운로드해 PV에 기록한다. restore-wait 자체는 데이터를 쓰지 않고 게이트 역할만 한다 — PodVolumeRestore 상태가 Completed로 바뀌면 그때 종료되고, 원래 앱 컨테이너가 시작된다.

$ kubectl get pod -l app=poc-app -o jsonpath='{.items[*].spec.initContainers[*].name}'
restore-wait
$ kubectl get podvolumerestores -n velero -o wide
NAME STATUS BYTES DONE TOTAL BYTES NODE UPLOADER TYPE
poc-restore-verify-2x9fk Completed 71 71 node3-gpu kopia

이 메커니즘 덕분에 앱 컨테이너는 항상 복원된 데이터가 준비된 상태에서 시작하게 된다.


복원 가이드

특정 namespace 복구

Terminal window
# 사용 가능한 백업 확인
velero backup get
# gitlab namespace만 복원
velero restore create --from-backup <backup-name> --include-namespaces gitlab

특정 리소스만 복구

Terminal window
# ConfigMap만 복원
velero restore create --from-backup <backup-name> \
--include-namespaces gitlab --include-resources configmaps

전체 복구 (재해 복구)

새 클러스터에 Velero + MinIO가 설치된 상태에서:

Terminal window
# Velero가 기존 백업을 자동 동기화
velero backup get
# 전체 복원
velero restore create --from-backup <backup-name>

주의: 복원은 기존 리소스를 덮어쓰지 않는다. 완전히 교체하려면 namespace를 먼저 삭제 후 복원한다.


리소스 사용량

컴포넌트CPU 요청메모리 요청스토리지
MinIO250m512Mi50Gi (Longhorn PVC)
Velero250m256Mi
NodeAgent (x3)100m each128Mi each
합계800m1.15Gi50Gi

정리

항목초기 설정최종 설정
볼륨 백업 방식CSI Snapshot (미구성)File-System Backup (kopia)
백업 상태PartiallyFailed (에러 44)Completed (에러 0)
PV 데이터 백업실패 (전부 skip)130개 볼륨 백업 성공
복원 검증미수행PoC 완료

향후 개선

현재 FSB로 충분하지만, CSI 스냅샷은 point-in-time 일관성과 속도 면에서 이점이 있다. kubespray에서는 변수 하나로 활성화 가능하다:

group_vars/k8s_cluster/k8s-cluster.yml
csi_snapshot_controller_enabled: true

이후 Longhorn용 VolumeSnapshotClass를 생성하면 CSI 스냅샷과 FSB를 병행하거나 전환할 수 있다.


참고 자료

관련 콘텐츠

댓글