soobook
KUBERNETES

Storage on Kubernetes

Pod은 죽으면 다시 뜬다. 같은 노드일 수도, 다른 노드일 수도 있다. 그럼 그 안에 있던 데이터는?

Pod은 일시적이지만 그 안의 데이터는 영속이어야 하는 경우가 대부분이다. DB의 row, 사용자가 올린 이미지, 학습용 데이터셋. 컨테이너가 사라져도 살아남아야 한다.

가장 단순한 답은 Pod이 떠 있는 노드의 디스크에 그냥 쓰는 것이다. 이게 Kubernetes의 hostPath 볼륨이다. 그리고 거의 대부분의 경우 이걸로는 부족하다.

Hostpath

hostPath는 노드의 특정 경로(/data/mysql 같은)를 컨테이너에 마운트한다.

volumes:
  - name: data
    hostPath:
      path: /data/mysql
      type: Directory

쓰기 쉽고 빠르다. 다만 한 가지 본질적인 문제가 있다. 데이터가 그 노드에만 있다는 점이다.

  • Pod이 다른 노드로 reschedule되면? 그 노드에는 /data/mysql이 없거나, 있더라도 다른 데이터가 들어 있다.
  • 노드가 죽으면? 데이터도 같이 죽는다. 백업해두지 않은 한.
  • 같은 데이터를 여러 Pod이 동시에 읽고 써야 한다면? hostPath는 한 노드에 묶여 있어 다른 노드의 Pod은 접근 못 한다.

hostPath는 해당 노드의 메타정보나 node-level 캐시에는 좋다. 하지만 영속적인 application 데이터에는 적합하지 않다. 노드 바깥으로 데이터를 빼낼 방법이 필요하다.

NFS

가장 오래된 답이 NFS(Network File System)다. 네트워크 너머의 디렉토리를 로컬 파일시스템처럼 마운트하는 프로토콜이다.

서버 한 대가 디스크를 export하면, 여러 클라이언트가 그걸 동시에 마운트해서 같은 파일을 읽고 쓴다. Kubernetes Pod 입장에서는 그냥 mount된 디렉토리이므로 애플리케이션 코드 변경이 0줄이다. open(), read(), write()가 그대로 먹힌다.

volumes:
  - name: shared
    nfs:
      server: nfs.internal
      path: /exports/shared

핵심은 여러 노드의 여러 Pod이 같은 디렉토리를 동시에 쓸 수 있다는 점이다. 노드가 죽어도 데이터는 NFS 서버에 있으니 Pod이 다른 노드로 옮겨가도 그대로 이어진다.

NAS(Network-Attached Storage)라는 단어를 자주 같이 듣는데, NAS는 파일 서버 어플라이언스(장비)를 가리키는 말이고 NFS는 그 위에서 돌아가는 프로토콜 중 하나다. 사내에 두는 파일 서버 박스가 NAS, 그 위에서 export 켜면 NFS. SMB(Windows 공유)도 같은 자리에 들어간다.

Block, File, Object

NFS는 큰 그림에서 File 스토리지에 속한다. 클라우드든 온프렘이든 영속 스토리지는 보통 셋 중 하나로 분류된다.

Block은 EBS, iSCSI가 대표다. 네트워크 너머에 있긴 하지만 클라이언트 입장에서는 raw block device로 보인다. OS가 그 위에 ext4, xfs 같은 파일시스템을 직접 얹는다. 한 host가 attach해서 쓰는 게 기본이고, 동시 마운트는 거의 안 되거나 매우 제한적이다. 대신 latency가 가장 낮다. DB의 데이터 디렉토리, 부팅용 디스크가 전형이다.

File은 NFS, EFS(AWS 매니지드 NFSv4)가 대표다. 이미 만들어진 파일시스템을 프로토콜 너머로 노출한다. 여러 노드, 여러 Pod이 동시에 마운트해서 같은 파일을 읽고 쓴다. 공유 업로드 폴더, 학습 데이터셋, 빌드 캐시처럼 공유가 본질인 워크로드에 잘 맞는다. 대신 메타데이터 연산(stat, readdir)이 네트워크 RTT를 타는 만큼 latency는 Block보다 느리다.

Object는 S3, GCS가 대표다. 파일시스템 자체가 없다. HTTP API로 키-바이트 객체 한 단위씩 PUT/GET한다. 디렉토리 트리도, 부분 수정도, append도 사실상 없다. 대신 사실상 무한 스케일과 매우 낮은 단가가 핵심이다. 정적 자산, 백업, 로그, 데이터레이크가 전형이다.

세 패밀리의 차이는 접근 단위와 동시 접근 모델에서 갈린다. 같은 1GB 파일이라도 다루는 방식이 다르다.

  • Block: “이 디스크는 내 거”
  • File: “여러 명이 같이”
  • Object: “URL로 받아간다”

PV, PVC, StorageClass, CSI

Kubernetes는 위 셋을 모두 같은 추상화로 다룬다. 사용자(Pod)는 백엔드가 EBS인지 NFS인지 모른 채 원하는 사양만 요청한다.

flowchart LR
    Pod -->|"마운트"| PVC
    PVC -->|"바인딩"| PV
    SC["StorageClass"] -.->|"동적 생성"| PV
    SC -->|"호출"| CSI["CSI driver"]
    CSI -->|"실제 볼륨 생성"| Backend["EBS / NFS / EFS / ..."]
  • PV (PersistentVolume): 클러스터에 있는 실제 스토리지 한 조각. 관리자가 미리 만들어두거나 동적으로 생성된다
  • PVC (PersistentVolumeClaim): 사용자가 원하는 사양을 적은 요청서. “10Gi, RWX 줘”. Pod은 PVC를 마운트한다
  • StorageClass: PV를 자동으로 만들어주는 템플릿. 어떤 백엔드 드라이버를 쓸지, 어떤 파라미터로 만들지 정의한다
  • CSI (Container Storage Interface): 백엔드 드라이버 표준. 코어 Kubernetes 코드를 손대지 않고도 임의의 block/file 스토리지를 K8s에 꽂을 수 있게 해준다

PVC를 만들 때 적는 accessModes가 백엔드 선택의 신호다.

  • ReadWriteOnce (RWO): 한 노드만 읽기/쓰기. Block 계열의 자연스러운 모드
  • ReadWriteMany (RWX): 여러 노드 동시 읽기/쓰기. File 계열에서만 가능
  • ReadOnlyMany (ROX): 여러 노드가 읽기만
  • ReadWriteOncePod (RWOP): 클러스터 통틀어 한 Pod만. CSI 한정

RWX가 필요하면 자동으로 File 계열을 골라야 한다는 뜻이다. EBS 같은 Block 백엔드는 RWX를 못 준다.

Object 스토리지는 이 추상화에 잘 안 맞는다. 파일시스템이 아니므로 PV/PVC로 마운트하는 게 부자연스럽다. S3 CSI driver(예: mountpoint-s3-csi-driver)가 있긴 한데, 보통은 애플리케이션이 SDK로 직접 호출하는 게 더 자연스럽다.

폐쇄망과 사내 네트워크

지금까지의 그림은 클라우드를 가정했다. 금융권, 공공처럼 외부 인터넷이 끊긴 환경이거나 온프렘 K8s 클러스터인 경우, EBS·EFS·S3 같은 매니지드는 후보에서 빠진다.

가장 흔한 풀이는 NAS 박스 한 대 두고 NFS로 export해서 K8s 노드들이 공유하는 방식이다. 이미 사내에 NAS가 있다면 export 경로 하나만 받아서 그대로 쓰면 된다. PVC 단위로 자동 발급까지 원하면 nfs-subdir-external-provisioner를 깔면 된다. PVC가 들어올 때마다 NAS 안에 ${namespace}-${pvcName}-${pvName} 디렉토리를 만들어준다.

K8s 노드와 NAS가 같은 L2/L3 네트워크 도메인 안에 있어야 한다. L2/L3는 OSI 모델의 데이터 링크 / 네트워크 계층, 쉽게 말해 같은 LAN(스위치 한 대 아래) 또는 라우터로 연결된 같은 사내 네트워크 안. 그리고 방화벽에 NFS 포트(2049/tcp)가 열려 있어야 한다. 클라우드에서는 자동으로 처리되던 부분이라 처음 만나면 당황스럽다.

References