,

[홈랩 검증기] 파드를 지워도 데이터는 남아 있었다 — PVC로 옮기기 전에 틀린 전제 두 개를 찾은 이야기

한 줄 요약: PVC로 옮기려다 “hostPath는 이미 영속”이고 “df로 본 디스크도 다른 파일시스템”이라는 전제 오류를 연달아 발견했고, 동적 PVC 대신 기존 디렉터리를 그대로 가리키는 정적 local PV로 방향을 바꿔 데이터를 한 바이트도 옮기지 않고 58초 만에 전환을 마쳤습니다.

이 글의 위치: 기술 블로그 → 인프라 · 로컬 서버 운영. 작성·검증 방식은 Editorial Policy 참조.
집 PC 한 대로 굴리는 홈랩 검증기 1편입니다. Docker Compose에서 K3s로 이주를 끝낸 그 서버에서, 이번엔 스토리지를 손봅니다. 이주 직후의 운영 후속은 백업 cron이 조용히 깨진 이야기에 있습니다.

이 글에서 한 일 — 한눈에

PVC를 붙이는 단순한 작업일 줄 알았는데, 시작 전에 잰 수치가 계획의 전제를 두 번 뒤집었습니다.

  1. 완료 조건을 먼저 재봤더니 작업 전에 이미 통과하고 있었습니다 — hostPath라 파드를 지워도 데이터가 남습니다.
  2. hostPath는 스토리지 시스템이 아닙니다 — 어느 디스크에 떨어지는지는 쿠버네티스가 아니라 노드의 마운트 테이블이 정합니다.
  3. 옮길 근거로 삼은 df 결과가 다른 파일시스템을 재고 있었습니다 — DB는 233GB 전용 SSD에 사용률 1%로 있었습니다.
  4. 원래 계획대로였다면 개선이 아니라 후퇴였습니다 — 전용 SSD에서 관측 스택과 공유하는 디스크로 내려갈 뻔했습니다.
  5. 세 가지 선택지를 표로 나란히 놓고 비교했습니다 — hostPath 유지 · 동적 PVC · 정적 local PV.
  6. 정적 local PV를 채택했습니다 — 기존 디렉터리를 그대로 가리켜 데이터를 한 바이트도 옮기지 않습니다.
  7. 기본값 두 개를 손봤습니다 — 회수 정책 Retain 과 노드 어피니티.
  8. 전환을 마쳤습니다 — 다운타임 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 크기는 줄어든 것처럼 보였습니다. 둘 다 정상이었는데 제 검증 방법이 그걸 구분하지 못했습니다.


참고

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다