,

[홈랩 검증기] 파드를 지워도 데이터는 남아 있었다 — 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-pathDelete입니다. kubectl delete pvc를 잘못 치면 그 순간 DB가 사라집니다. 정적 PV를 직접 선언하는 김에 Retain으로 박아둡니다.

securityContext.fsGroup은 넣지 않습니다. 기존 PGDATA는 uid 70 / gid 0 / 0700이었습니다. fsGroup을 주면 파드가 뜰 때마다 재귀 chown이 걸립니다. 데이터가 커질수록 기동이 느려지고, 얻는 것도 없습니다.

그리고 용량은 넉넉히 잡았습니다. local-pathallowVolumeExpansion: 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 크기는 줄어든 것처럼 보였습니다. 둘 다 정상이었는데 제 검증 방법이 그걸 구분하지 못했습니다.


참고

답글 남기기

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