Velero로 k3s 백업하기 - 매일 자동 백업 설정부터 홈서버 3중 백업 구성까지
쿠버네티스를 직접 운영하면 언젠가 이런 생각이 든다. "이 노드가 갑자기 죽으면 나는 뭘 잃지?" 나는 노트북 한 대에 k3s를 올려 블로그, Gitea, 메일, 게임 같은 서비스를 전부 돌린다. 노드가 한 대뿐이라 디스크 하나가 죽으면 곧바로 전부 사라질 수 있는 구조다. 그래서 백업을 붙였고, 그 중심에 Velero가 있다.
이 글에서는 Velero가 무엇이고 어떻게 동작하는지, 내 클러스터에 어떻게 구성했는지, 그리고 Velero만으로는 부족해서 어떤 백업을 더 붙였는지 정리한다. 아래 출력은 전부 지금 운영 중인 클러스터에서 가져왔다.
Velero란
Velero는 쿠버네티스 클러스터의 리소스(Deployment, Service, ConfigMap 등)와 볼륨 데이터를 백업하고 복원해 주는 오픈소스 도구다. 백업 결과는 S3 호환 오브젝트 스토리지에 저장한다. 크게 세 가지 구성요소로 이루어진다.
- velero 서버(Deployment): Backup, Schedule, Restore 같은 CRD를 보고 실제 백업을 수행한다.
- BackupStorageLocation(BSL): 백업을 저장할 위치. S3 버킷 주소와 인증 정보를 가리킨다.
- node-agent(DaemonSet): 볼륨 안의 파일을 직접 읽어 백업하는 에이전트. 파일시스템 백업(kopia)에 쓰인다.
동작은 단순하다. 백업을 만들면 Velero가 API 서버에서 리소스를 모아 JSON으로 올리고, 볼륨은 node-agent가 파일 단위로 읽어 중복 제거와 압축을 거쳐 같은 버킷에 올린다. 복원은 그 반대다.
볼륨 백업 방식 - 스냅샷 vs 파일시스템 백업
Velero에서 가장 먼저 정해야 하는 건 볼륨을 어떻게 백업할지다.
- CSI/클라우드 스냅샷: 클라우드 디스크나 스냅샷을 지원하는 스토리지에서 쓴다. 빠르지만 스토리지 드라이버 지원이 필요하다.
- 파일시스템 백업(fs-backup): Pod에 마운트된 볼륨을 node-agent가 파일 단위로 읽는다. 스냅샷을 못 쓰는 환경에서도 되고, 스토리지 종류와 무관하다.
k3s 기본 스토리지(local-path)는 스냅샷 기능이 없다. 그래서 나는 파일시스템 백업을 골랐다. 스케줄 설정의 snapshotVolumes: false와 defaultVolumesToFsBackup: true가 그 선택이다.
내 구성 - 매일 새벽 4시 전체 백업
설치는 Helm이나 velero install로 하면 된다. 핵심은 백업 저장소다. 클라우드 S3를 쓰면 편하지만 비용이 들고 외부로 데이터가 나간다. 나는 클러스터 안에 MinIO를 띄워 S3 호환 저장소로 썼다.
$ kubectl -n velero get backupstoragelocation
NAME PHASE LAST VALIDATED AGE DEFAULT
default Available 58s 29d true
$ kubectl -n velero get deploy,ds
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/velero 1/1 1 1 29d
daemonset.apps/node-agent 1 1 1 ...
PHASE가 Available이면 Velero가 버킷에 정상적으로 접근한다는 뜻이다. 백업이 안 될 때 제일 먼저 볼 곳이 여기다.
스케줄은 하루에 한 번이다.
$ kubectl -n velero get schedule daily-full -o jsonpath='{.spec}'
{"schedule":"0 19 * * *",
"template":{
"defaultVolumesToFsBackup":true,
"excludedNamespaces":["kube-system","minio"],
"snapshotVolumes":false,
"ttl":"336h0m0s"}}
0 19 * * *: 크론은 UTC 기준이라 한국 시간으로 매일 새벽 4시다. 시간대 때문에 한 번 헷갈렸던 부분이라 적어 둔다.ttl: 336h: 14일 보관 후 자동 삭제. 디스크가 무한하지 않으니 보관 기간은 꼭 정해야 한다.excludedNamespaces:kube-system은 제외했고,minio네임스페이스도 제외했다. 백업 저장소 자신을 백업 대상에 넣으면 의미가 없고 용량만 늘기 때문이다.
실제 백업 결과는 이렇다.
$ kubectl -n velero get backup
daily-full-20261010190008 Completed 2026-10-10T19:00:08Z ~ 19:05:26Z
daily-full-20261011190010 Completed 2026-10-11T19:00:10Z ~ 19:05:48Z
daily-full-20261012190011 Completed 2026-10-12T19:00:11Z ~ 19:05:40Z
1,241개 항목이 백업되고 5분 남짓 걸린다. 그리고 14일치가 쌓여 있다. 한 달 가까이 돌렸는데 Completed가 끊긴 적은 없다.
Velero만으로는 부족했다
여기서 끝내도 될 것 같지만 구조를 따져 보면 구멍이 보인다.
- 백업 저장소(MinIO)가 같은 노트북 안에 있다. 노트북이 통째로 죽으면 백업도 같이 죽는다.
- 복원하려면 MinIO부터 살려야 한다. 백업이 들어 있는 MinIO 자체가 클러스터 안에 있으니, 클러스터가 없으면 백업을 꺼낼 방법부터 막힌다.
- Velero는 k3s 자체의 상태는 백업하지 않는다. 클러스터 토큰, 인증서,
/etc설정,/root의 작업 파일은 범위 밖이다.
그래서 Velero를 1층으로 두고, 그 아래로 백업 계층을 두 개 더 만들었다.
2층 - 셸 스크립트로 "클러스터 밖" 데이터 백업
매일 새벽 3시 30분에 크론으로 스크립트를 돌린다. Velero보다 30분 먼저 도는 셈이다. 담는 내용은 Velero가 다루지 않는 것들이다.
- k3s 서버 디렉터리(토큰, 인증서)와 클러스터 DB(sqlite)
/etc와/root(캐시는 제외)- 블로그 PostgreSQL 덤프 (
pg_dumpall) - Gitea 데이터 (저장소 전체)
- 클러스터 오브젝트 YAML 내보내기
하루 약 1GB이고 /home/backup/날짜/에 저장하며 7일치를 보관한다. 스크립트를 만들면서 겪은 함정이 몇 가지 있어서 적어 둔다.
- sqlite는 파일을 그냥 복사하면 안 된다. 쓰는 중에 복사하면 깨진 파일이 나올 수 있어서
sqlite3 .backup명령으로 일관된 복사본을 만든다. - tar는 "파일이 읽는 도중 바뀜"이면 종료 코드 1을 돌려준다. 실행 중인 서비스 파일을 묶을 때 흔한 일이라 1은 실패로 치지 않고, 2 이상만 실패로 처리한다.
- 크론은 PATH가 짧다.
kubectl을 못 찾아서 조용히 실패하니 크론 파일 맨 위에PATH=를 꼭 넣어야 한다. - 실패한 백업은 이름을 바꿔 둔다. 실패하면 폴더를
.FAILED로 바꿔서, 성공한 날짜와 한눈에 구분되게 한다. - 덤프를 만든 뒤에는 열어서 확인한다.
tar -tzf로 목록이 나오는지, SQL 덤프에CREATE TABLE이 있는지,pragma integrity_check가ok인지 본다. 파일이 생겼다고 백업이 된 게 아니다.
3층 - USB에 한 벌 더
같은 디스크 안의 백업은 디스크가 죽으면 소용이 없다. 그래서 스크립트 마지막에 USB 저장장치로 복사하는 단계를 붙였다. 8GB USB를 ext4로 포맷해서 마운트해 두었고, 3일치만 보관한다.
- 복사한 뒤 원본과 사본의 체크섬을 비교해서 같을 때만 확정한다. 다르면 사본을 지운다.
- 용량이 작으니 새로 쓰기 전에 오래된 것부터 지운다.
- USB가 안 꽂혀 있으면 건너뛰기만 하고, 로컬 백업의 성공 여부에는 영향을 주지 않는다.
$ ls /mnt/backup-usb/backup
2026-10-11 2026-10-12 2026-10-13 RESTORE.md
RESTORE.md도 같이 복사해 둔다. 장애가 났을 때 이 문서를 보고 어떻게 되살릴지 적어 둔 절차서다. 그 순간의 나는 아마 당황해 있을 테니, 순서를 문서로 남기는 게 중요하다.
정리하면 이런 구조다
| 계층 | 시각 (KST) | 내용 | 보관 |
|---|---|---|---|
| 1. Velero → MinIO | 04:00 | 클러스터 리소스 + 볼륨(kopia) | 14일 |
| 2. 스크립트 → /home/backup | 03:30 | k3s 상태, /etc, /root, PostgreSQL, Gitea | 7일 |
| 3. USB 복사 | 스크립트 끝에 | 2번 결과물 전체 | 3일 |
이 구조가 막아 주는 것과 못 막는 것을 솔직하게 적어 둔다. 디스크 한 개가 죽거나 파일을 잘못 지운 경우는 대부분 복구된다. USB가 있으니 노트북 내부 디스크가 둘 다 죽어도 핵심 데이터는 남는다. 반면 집에 불이 나거나 도난당하는 경우는 USB도 같은 장소에 있으니 못 막는다. 외부(다른 장소나 클라우드)에 사본이 없기 때문이다. 일단 여기까지는 감수하기로 했고, 다음 단계는 암호화한 사본을 외부로 보내는 것이다.
Velero 운영 팁
- 보관 기간(TTL)을 반드시 정한다. 안 정하면 백업이 계속 쌓여 저장소가 가득 찬다.
- 백업 저장소는 백업 대상에서 뺀다. 위에서
minio네임스페이스를 제외한 이유다. - Warning이 조금 있다고 실패는 아니다. 내 백업도 매번 50~60개의 경고가 붙지만
Completed다. 다만 경고 내용은velero backup describe <이름>으로 가끔 읽어 본다. - DB는 파일 복사보다 덤프가 안전하다. 볼륨 백업은 쓰는 도중의 파일을 가져올 수 있어서, PostgreSQL은
pg_dumpall로 따로 뽑아 둔다. - 알림을 건다. 백업은 조용히 실패한다. 마지막 백업 시각을 확인하는 방법을 하나는 만들어 둔다.
kubectl get schedule의LASTBACKUP열만 봐도 된다.
아직 안 한 것 - 복구 리허설
솔직히 말하면 실제 복구는 아직 해보지 않았다. 백업이 잘 쌓이고 있다는 것과, 그 백업으로 서비스를 되살릴 수 있다는 것은 다른 이야기다. "백업은 복구해 봐야 백업"이라는 말이 있는데, 지금 내 상태는 "백업 파일이 있다"까지다. 복원 테스트는 테스트용 네임스페이스에 velero restore create --from-backup ...로 한 앱만 되살려 보는 것부터 시작할 계획이다. 해 보면 이 글에 결과를 덧붙이겠다.
마치며
Velero 하나로 모든 게 해결되지는 않는다. 쿠버네티스 리소스와 볼륨은 Velero가 잘 맡아 주지만, 클러스터 바깥의 설정과 백업 저장소 자체의 안전은 따로 챙겨야 한다. 내 경우에는 Velero(1층) + 스크립트(2층) + USB(3층)으로 나눴고, 각 층이 서로의 약점을 가려 주는 구조가 됐다. 홈서버나 소규모 k3s를 운영한다면 가장 먼저 "내 백업이 어디에 있고, 그 장소가 죽으면 어떻게 되는가"부터 점검해 보길 권한다.
댓글 0
글에 대한 질문이나 오류 제보는 문의하기로 보내 주세요.