한 줄 요약: PVC로 옮기려다 “hostPath는 이미 영속”이고 “
df로 본 디스크도 다른 파일시스템”이라는 전제 오류를 연달아 발견했고, 동적 PVC 대신 기존 디렉터리를 그대로 가리키는 정적 local PV로 방향을 바꿔 데이터를 한 바이트도 옮기지 않고 58초 만에 전환을 마쳤습니다.
이 글의 위치: 기술 블로그 → 인프라 · 로컬 서버 운영. 작성·검증 방식은 Editorial Policy 참조.
집 PC 한 대로 굴리는 홈랩 검증기 1편입니다. Docker Compose에서 K3s로 이주를 끝낸 그 서버에서, 이번엔 스토리지를 손봅니다. 이주 직후의 운영 후속은 백업 cron이 조용히 깨진 이야기에 있습니다.
이 글에서 한 일 — 한눈에
PVC를 붙이는 단순한 작업일 줄 알았는데, 시작 전에 잰 수치가 계획의 전제를 두 번 뒤집었습니다.
- 완료 조건을 먼저 재봤더니 작업 전에 이미 통과하고 있었습니다 —
hostPath라 파드를 지워도 데이터가 남습니다. hostPath는 스토리지 시스템이 아닙니다 — 어느 디스크에 떨어지는지는 쿠버네티스가 아니라 노드의 마운트 테이블이 정합니다.- 옮길 근거로 삼은
df결과가 다른 파일시스템을 재고 있었습니다 — DB는 233GB 전용 SSD에 사용률 1%로 있었습니다. - 원래 계획대로였다면 개선이 아니라 후퇴였습니다 — 전용 SSD에서 관측 스택과 공유하는 디스크로 내려갈 뻔했습니다.
- 세 가지 선택지를 표로 나란히 놓고 비교했습니다 —
hostPath유지 · 동적 PVC · 정적 local PV. - 정적 local PV를 채택했습니다 — 기존 디렉터리를 그대로 가리켜 데이터를 한 바이트도 옮기지 않습니다.
- 기본값 두 개를 손봤습니다 — 회수 정책
Retain과 노드 어피니티. - 전환을 마쳤습니다 — 다운타임 58초, 복사 0바이트.
아래부터 순서대로 풀어씁니다.
"PVC를 붙여야 데이터가 안전해집니다"
K3s로 이주한 뒤 미뤄둔 숙제가 하나 있었습니다. Postgres가 아직 PVC를 안 쓰고 있다는 것입니다. 그래서 계획을 이렇게 세웠습니다.
1단계(필수) — 영속성 확보. 파드가 재생성돼도 DB 데이터가 유지되도록 구성.
완료 조건: Pod 삭제 → 새 Pod 생성 → 기존 데이터 그대로 존재.
문장만 보면 흠잡을 데가 없습니다. 그런데 작업을 시작하기 전에 현재 상태를 재보다가, 이 완료 조건이 작업하기 전에 이미 통과한다는 걸 알게 됐습니다.
volumes:
- name: data
hostPath:
path: /srv/state/postgres
type: Directory
emptyDir가 아니라 hostPath였습니다. 파드를 지웠다 만들어도 데이터는 노드 디스크에 그대로 남습니다. 영속성은 이미 확보돼 있었습니다.
hostPath는 스토리지 시스템이 아닙니다
여기서 개념을 한 번 정리하고 갈 필요가 있었습니다. hostPath는 별도의 스토리지 구현체가 아닙니다. 프로비저너도, 용량 개념도, 복제도 없습니다. "노드의 이 디렉터리를 컨테이너 안으로 그대로 bind mount 하라"는 지시 한 줄입니다.
그래서 중요한 결론이 따라옵니다 — 그 경로가 어느 디스크에 떨어지는지는 쿠버네티스가 아니라 노드의 마운트 테이블이 결정합니다.
이 사실이 두 번째 오독으로 이어집니다.
두 번째 전제도 틀렸습니다
영속성이 이미 있다면, 그럼에도 PVC로 옮길 이유가 있어야 합니다. 저는 이렇게 판단했습니다.
$ df -h /srv
Filesystem Size Used Avail Use%
/dev/sdb2 23G 7.9G 14G 37% ← 루트 파티션, 여유 14G
"DB가 루트 파티션 위에 있고 여유가 14G뿐이다. 루트가 차면 노드 전체가 멈춘다. 이건 옮겨야 한다." 꽤 설득력 있는 근거였습니다.
그런데 이게 틀렸습니다. df에 넘긴 경로가 /srv였고, 실제 데이터는 /srv/state/postgres에 있었습니다. /srv와 /srv/state는 서로 다른 파일시스템이었습니다.
$ lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT
sda 232.9G ext4 /srv/state ← DB 데이터. 전용 디스크였다
sdb 931.5G
├─sdb2 23.3G ext4 /
├─sdb3 9.3G ext4 /var
└─sdb6 895.6G ext4 /home
sdc 1.8T ext4 /data ← 동적 PVC(local-path)가 쓰는 곳
DB는 233GB짜리 전용 SSD에 있었고 사용률은 1%였습니다. 루트 파티션 이야기는 처음부터 성립하지 않았습니다.
교훈: 마운트는 부모 경로가 아니라 그 경로 자체로 재야 합니다. 하위에 다른 디스크가 마운트돼 있으면 부모를 재는 순간 완전히 다른 디스크의 숫자를 보게 됩니다.
findmnt -T /srv/state/postgres # 항상 경로 그 자체로
이 정정이 작업을 통째로 바꿨습니다
원래 계획은 클러스터 기본 StorageClass인 local-path로 동적 PVC를 만드는 것이었습니다. 그런데 local-path가 프로비저닝하는 위치는 /data(sdc1)입니다. 그 디스크에는 이미 Prometheus·Loki·Tempo의 볼륨이 들어 있습니다.
즉 원래 계획대로 했다면, 전용 SSD에 혼자 있던 DB를 관측 스택과 디스크를 나눠 쓰는 곳으로 강등시키는 셈이었습니다. 개선이 아니라 후퇴입니다.
세 가지 선택지를 나란히 놓기
여기서 선택지를 정리했습니다.
| 항목 | hostPath (현행) | 동적 PVC (local-path) |
정적 local PV (채택) |
|---|---|---|---|
| 데이터 이동 | — | 필요 (145MB 복사) | 없음 |
| 데이터가 놓이는 디스크 | sda1 (전용 SSD) | sdc1 (관측 스택과 공유) | sda1 유지 |
| 용량이 매니페스트에 드러나는가 | ❌ 개념 자체가 없음 | ✅ | ✅ (단, 강제되지는 않음) |
| PVC/StatefulSet 문법 사용 | ❌ | ✅ | ✅ |
| 노드가 늘면 | 잘못된 노드에 뜨면 빈 디렉터리로 새 DB 초기화 | 노드 고정 | nodeAffinity로 고정 |
| 복사 손상 위험 | — | 있음(검증 게이트 필요) | 원천적으로 없음 |
| 관리 비용 | 낮음 | 낮음(자동) | PV를 직접 선언·관리 |
정적 local PV는 이미 있는 디렉터리를 PV로 다시 선언하는 방식입니다. 프로비저너가 새 공간을 만드는 게 아니라, "이 경로가 볼륨이다"라고 쿠버네티스에 알려주는 것입니다. 그래서 복사가 아예 없습니다.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-srv
provisioner: kubernetes.io/no-provisioner # 정적 PV 전용
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: app-postgres-data
spec:
capacity:
storage: 100Gi # local PV 에서 capacity 는 강제되지 않는 라벨이다
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: local-srv
local:
path: /srv/state/postgres # ← 기존 데이터가 있는 그 경로
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [node1]
기본값을 그대로 두면 안 되는 것 두 가지
persistentVolumeReclaimPolicy: Retain. 클러스터 기본 StorageClass인 local-path는 Delete입니다. kubectl delete pvc를 잘못 치면 그 순간 DB가 사라집니다. 정적 PV를 직접 선언하는 김에 Retain으로 박아둡니다.
securityContext.fsGroup은 넣지 않습니다. 기존 PGDATA는 uid 70 / gid 0 / 0700이었습니다. fsGroup을 주면 파드가 뜰 때마다 재귀 chown이 걸립니다. 데이터가 커질수록 기동이 느려지고, 얻는 것도 없습니다.
그리고 용량은 넉넉히 잡았습니다. local-path는 allowVolumeExpansion: false라 나중에 못 늘립니다. 정적 local PV에서 capacity는 강제되지 않는 라벨이지만, 확장 불가라는 관례에 맞춰 처음부터 크게 선언했습니다.
전환 결과 — 다운타임 58초, 데이터 무이동
절차는 단순했습니다. 백업으로 게이트를 걸고, 기준값을 재고, Deployment를 내린 뒤 StatefulSet을 올립니다.
Deployment를 먼저 지우는 게 중요합니다. 둘이 같은 PGDATA를 동시에 물면 데이터가 손상됩니다. postmaster.pid 잠금이 막아줄 것 같지만, 컨테이너마다 IPC 네임스페이스가 달라 그 검사를 신뢰할 수 없습니다.
11:50:36 Deployment replicas=0 ← 다운타임 시작
11:50:4x Deployment 삭제
11:51:34 StatefulSet Ready ← 다운타임 종료 (58초)
검증은 세 가지로 했습니다.
| 확인 항목 | 결과 |
|---|---|
| 마운트가 의도한 디스크인가 | /dev/sda1 /var/lib/postgresql/data ext4 — 전용 SSD 유지 |
| 데이터가 그대로인가 | DB 11개 크기 바이트 단위 일치 |
| 완료 조건 | 파드 삭제 → 재생성 → 데이터 동일 ✅ |
여기서 하마터면 오진할 뻔한 게 하나 있었는데, 그건 다음 편의 주제라 여기서는 넘어갑니다.
정리 — 옮기기 전에 왜 옮기는지를 재라
이 작업에서 실제로 얻은 것은 "PVC를 붙였다"가 아닙니다. 전제를 두 번 고친 것입니다.
- 완료 조건으로 세운 "파드 삭제 후 데이터 유지"는 작업 전에 이미 통과하는 조건이었습니다.
- 근거로 삼은 "여유 14G"는 다른 디스크의 숫자였습니다. 이걸 못 잡았다면 전용 SSD에 있던 DB를 공용 디스크로 옮겼을 겁니다.
두 번째가 특히 무섭습니다. 잘못된 근거가 그럴듯하면, 작업은 성공적으로 끝나고 아무도 문제를 모릅니다. 몇 달 뒤 디스크가 시끄러워질 때에야 "왜 여기 있지?"가 됩니다.
그래서 이 시리즈의 규칙을 하나 세웠습니다. 믿음은 측정으로만 바꾼다. df가 아니라 findmnt -T, 추측이 아니라 lsblk.
다음 편에서는 전환 직후에 만난 함정을 다룹니다. 모든 파드가 READY 1/1인데 애플리케이션은 DB에 닿지 못하고 있었고, DB 크기는 줄어든 것처럼 보였습니다. 둘 다 정상이었는데 제 검증 방법이 그걸 구분하지 못했습니다.
참고
- Kubernetes — Volumes: hostPath — hostPath의 성격과 경고
- Kubernetes — Local Persistent Volumes —
local볼륨과nodeAffinity요구사항 - Kubernetes — Reclaiming —
Retain/Delete동작 - Rancher local-path-provisioner — k3s 기본 StorageClass 구현
- PostgreSQL — Creating a Database Cluster — 데이터 디렉터리 권한 요구사항
util-linuxfindmnt(8)—-T옵션으로 특정 경로의 마운트를 조회
답글 남기기