Jellyfin_Storage

Jellyfin 미디어 스토리지 구조 변경 정리 (RWX → RWO 전환)

🧾 목적

  • Jellyfin의 미디어 저장소를 외장하드를 기반으로 안정적으로 운영하기 위해, 기존 NFS(RWX) 방식에서 Longhorn 기반 RWO 방식으로 구조를 변경함
  • Git + Argo CD + Helm으로 배포되는 홈 미디어 서버 인프라의 일환으로 작업함
  • 향후에도 이 구조를 유지하고 실수를 반복하지 않도록 문서화함

✅ 작업 배경 및 원인

  • 초기에는 여러 Pod에서 동시에 읽고 쓸 수 있도록 하기 위해 RWX 모드(NFS 서버 + PVC) 구조를 시도했음
  • 그러나 Helm으로 배포된 nfs-server-provisioner가 자동으로 PVC를 만들면서 수동 생성한 Longhorn PVC와 충돌함
  • 또한 PVC 요청 용량이 실제 볼륨보다 커서 바인딩 자체가 실패했음
  • 문제를 반복적으로 해결하려 했지만, 결국 안정성을 위해 RWX 대신 RWO 기반 단일 Pod 전용 구조로 변경 결정

✅ 작업 순서 요약

  1. 기존 NFS 기반 RWX 구성 리소스 삭제 시도 (kubectl)
  2. Argo CD가 리소스를 다시 살려냄 → Git에서 Helm values 삭제 + Argo CD Sync로 완전 제거
  3. Longhorn UI에서 외장하드 디스크에 jellyfin-disk 태그 부여
  4. PVC 요청 크기를 470Gi로 줄이고 Longhorn Volume 새로 생성
  5. PVC를 jellyfin-media-pvc로 정의하고 수동으로 apply
  6. Helm values.yaml에서 PVC를 참조하도록 설정 변경
  7. Git 커밋 후 Argo CD Sync로 적용
  8. 파일 업로드 테스트 (scp, debug pod, UI 업로더 등)

🧹 삭제 및 수정한 항목

❌ 삭제 대상

kubectl delete pod home-media-nfs-server-provisioner-0 -n media      # → 삭제했지만 다시 살아남
kubectl delete pvc nfs-media-pvc -n media
kubectl delete pvc media-longhorn-pvc -n media                       # (선택적으로)

✅ Argo CD 기준으로 실제 제거한 방법

  1. apps/home-media-server/values.yaml에서 nfs-server-provisioner 항목 제거
  2. Git add/commit/push
  3. Argo CD UI → home-media-server 앱 → Sync → 'Prune' 체크 → Sync 실행
  4. 이후 관련 Pod, PVC, Service, StatefulSet 등 완전히 제거됨

✅ 최종 구조 (RWO 기반 Jellyfin 스토리지)

💾 스토리지 흐름

[외장하드 (/mnt/longhorn-disk)]
    └─ Longhorn 디스크 (tag: jellyfin-disk)
        └─ Volume: jellyfin-media-volume (470Gi)
            └─ PVC: jellyfin-media-pvc
                └─ Jellyfin Pod의 /media 에 마운트됨

⚙️ PVC YAML (base/jellyfin-media-pvc.yaml)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jellyfin-media-pvc
  namespace: media
  annotations:
    purpose: "jellyfin-only"   # jellyfin 전용임을 명시
spec:
  accessModes:
    - ReadWriteOnce            # 외장하드는 RWO만 가능
  storageClassName: longhorn
  resources:
    requests:
      storage: 470Gi           # 외장하드 용량 고려해서 470Gi로 제한

🧩 Helm values.yaml (apps/home-media-server/values.yaml)

persistence:
  config:
    enabled: true
    existingClaim: jellyfin-config-pvc
  media:
    enabled: true
    existingClaim: jellyfin-media-pvc

🧠 디스크 태그 설정 (Longhorn UI)

  • 위치: Nodes → oldbear → /mnt/longhorn-disk → Edit
  • Tag 입력: jellyfin-disk
  • 효과: 이 디스크는 jellyfin-disk 태그가 설정된 볼륨만 저장 가능 → jellyfin 전용 보장

📂 파일 업로드 방법 정리

📌 방법 1: Debug Pod (PVC 마운트된 쉘)

kubectl run -n media jellyfin-media-debug --rm -it --image=ubuntu --overrides='...'

📌 방법 2: 외부 PC → oldbear 노드 직접 접근

scp ~/Videos/Movie.mp4 igotoo@192.168.0.100:/mnt/longhorn-disk/pvc-xxx/data/

⚠️ 주의: 이 방식은 Longhorn 내부 디렉토리(/mnt/.../replicas/...)에 직접 접근하는 것으로, 권장되지 않으며 향후 데이터 무결성에 문제가 생길 수 있음.
→ 이 경로는 Longhorn의 internal replica 영역으로 Jellyfin이 인식하지 않음.
→ 반드시 Pod 내 /media 경로를 사용하거나 Upload API를 통해 복사해야 안전함.

📌 방법 3: 웹 업로더 준비 중

  • /upload API 제공하는 Node.js 서버(Express)
  • Jellyfin ingress에 /upload 경로로 붙여서 클라이언트에서 직접 업로드 가능
  • React UI 앱을 통해 파일 선택 + 전송 예정

🧨 과거 실수 & 교훈

실수 교훈
RWX PVC 요청 용량이 실제 Longhorn 볼륨보다 큼 PVC 요청은 항상 실제 볼륨보다 작아야 함
Helm Chart가 StatefulSet과 PVC 자동 생성 수동 PVC와 충돌할 수 있음, chart 구조 미리 검토할 것
kubectl delete만 하면 되는 줄 앎 GitOps 환경에선 반드시 Git 수정 + Argo CD Sync 필요
디스크 Tag 미지정으로 외장하드에 다른 볼륨 올라감 외장하드는 tag로 독점 제한 필요
Longhorn replica 디렉토리에 직접 scp 복사 시도 해당 영역은 raw volume 이미지 저장소로 직접 접근 불가. 반드시 Pod 내부 경로 사용

🔚 결론

  • Jellyfin은 현재 RWO PVC로 외장하드 단독 사용 중이며 매우 안정적
  • 과거 RWX 구성은 실패 이유까지 명확히 파악하고 제거함
  • upload-server 배포 준비도 GitOps 환경에 맞춰 Helm chart 형태로 설계 중
  • 홈 미디어 서버 스토리지 구성은 완전히 통제 가능 상태에 도달함