본문 바로가기
MLOps

[MLOps] Kubernetes (k8s) Persistent Volume Share w/ nfs

by ML_MJSHIN 2021. 11. 23.

  틈틈히 k8s 와 관련된 기본적인 내용들에 대해서도 정리해두고자 합니다. 정리를 안해두면 잊어버리는 것도 있고, 정리하면서 다시 개념을 공부하게 되는 것도 있는 것 같습니다. 이 포스팅은 대부분 References 에 있는 블로그들의 내용을 다시 정리한 포스팅입니다. 


  최근 ML 관련 기업들에서 Kubernetes 는 가장 필수 요소가 되고 있습니다. multi gpu 서버들을 여러대 두는 것은 당연히 기본이지만 kuberentes 를 활용해서 물리적으로 분리된 multi gpu 서버를 마치 하나의 server처럼 인식해서 남는 리로스 (gpu, cpu ..... ) 가 없이 ML 모델을 학습할 수 있도록 인프라를 구축하는 것이 대세가 되었습니다.

 

 이때, 저희 회사의 경우는 ML 모델을 학습하는 server 들과 실제 학습에 사용될 데이터가 담긴 Data Lake의 저장 server, 그리고 Log들을 저장하는 server 가 분리되어 있습니다. 

 

 이 경우, k8s의 Pod 에 의해 생성된 container들이 해당 data & log server의 파일 시스템에 접근하여 data를 읽고 log 파일을 작성해야 합니다. 

 

 이러한 방법에는 여러가지 방식이 있지만 nfs 를 통해서 이를 쉽게 수행 할 수 있습니다. 이 포스팅에서는 nfs 를 통해서 Pod 들이 원하는 server의 파일 시스템에 접근하는 k8s 사용 방법에 대해서 간단하게 정리하였습니다.


  kubernetes 를 활용할 때 저장 공간 (저장소) 와 관련된 k8s object가 존재하며 이는 Pod 의 Container 들이 자유롭게 접근하여 데이터를 읽고 쓸 수 있는 디렉토리를 의미합니다. 

 

  k8s에서 Pod의 Container들은 stateless (종료되면 상태가 남지 않는 ...) 특징을 가지고 있습니다. 그러나, MLOps에서 실험을 진행하는 Container가 존재하는 경우 해당 Container가 실험을 진행한 결과가 어딘가에 저장되어 남아있어야 의미가 있습니다. 이때, k8s volume object를 사용해서 Stateless한 Container의 결과를 저장해둘 수 있습니다. 이 Volume object를 활용해서 Pod 내의 Container들이 서로 정보를 공유할 수 있습니다. 

 

 이때, k8s가 동작하는 node의 파일 시스템에 접근할 수도 있으며 Amazon Elastic Block Store (EBS)같은 스토리지를 동적으로 생성하여 사용할 수도 있습니다. [1] 그림으로 보면 아래와 같은 구조가 됩니다.

쿠버네티스 #5 - 디스크 (볼륨/Volume) (tistory.com) [3]

 그림에서 PV는 Infra Engineer 가 생성한 물리적인 디스크를 의미하며, Pod의 Volume과 PV를 연결해주는 relation 역할을 PVC가 수행합니다.

 

위와같이 k8s volume object를 사용하는 방법은 크게 2가지 (Static vs Dynamic) 으로 나눌 수 있습니다. 

 

 첫번째로 Pod들이 생성될 때 Persistent Volume을 Infra Engineer가 미리 수동으로 만들어 두고 PVC를 Application Engineer가 적용하는 방법인 Static Provisioning이 있습니다. 이는 위 그림에서 표현하는 예시입니다.

 

 여기서 Provisioning이란 용어가 의미하는 바는 디스크 저장소에 PV 를 위한 공간을 확보하고 PV를 생성하는 것을 의미합니다. 

 

 두번째로, Dynamic Provisioning은 application (사용자)가 PVC를 통해서 원하는 PV에 대한 정보를 제공하면 그때 요청된 용량에 맞는 PV를 생성합니다.  

 

[3]

  위의 Static Provisioning과 달리 StorageClass를 사용해서 Request 하는 부분이 추가되어있습니다. 미리 PV가 생성되어있지 않기 때문에 PVC가 Select하는 것이 아니라 StorageClass를 통해서 원하는 PV 를 요청하고 그게 생성되면 Bind하는 순서를 따릅니다. (Request -> Creation -> Bind)

 

  PVC를 정의할 때 Storage class를 지정하지 않으면, 디폴트로 설정된 storage class 값을 사용하게 되는데 Cloud 환경 (AWS, GCP 등)을 사용하는 경우에는 Kubernetes 의 Default StorageClass가 존재합니다.

 

  반면, kubernetes 를 on-premise 로 구성해서 AI Engine을 작동시키고자 할 때는 Default StorageClass가 존재하지 않습니다. 그리고, k8s 에서 AI 실험을 위한 Pod 들이 생성되어 실험을 진행하는 경우 Data Server (편의상 Data와 Log가 존재하는 서버를 Data Server라고 부르도록 하겠습니다.) 에 Data와 Log를 저장해야 하는 중요한 문제가 있습니다.

 

  이 포스팅에서는 Data Server의 공유 폴더를 nfs로 공유하여 Pod의 Volume에 Dynamic Provisioning을 통해서 해당 폴더를 bind하는 방법을 정리하도록 하겠습니다.

 

  가장 먼저 할 일은 Provisioner Pod를 위한 Service Account를 생성하는 것입니다. Service Account란, Kubernetes의 API를 호출하는 모든 행위에 대한 권한을 의미합니다. API를 호출한다는 의미는 즉 Kubernetes의 Object를 컨트롤 할 수 있는 권한이 있다는 것으로 생각할 수 있습니다. 

 

 이때, 실제 사람 사용자(User)와 다르게 주로 k8s의 시스템이 k8s의 API를 호출하는 형식을 취할 때 Service Account 를 생성합니다. [4]

K8S 보안 "인증/인가" (tistory.com)

 

 Dynamic Provisioning을 수행할 수 있는 권한이 필요하므로 PV와 관련된 기능들에 대해서 할당해줍니다. [3]

아래의 sa.yaml 을 만듭니다. (저는 [1], [2], [3]의 블로그를 참조하였습니다.) 

kind: ServiceAccount
apiVersion: v1
metadata:
  name: nfs-pod-provisioner-sa
---
kind: ClusterRole # Role of kubernetes
apiVersion: rbac.authorization.k8s.io/v1 
metadata:
  name: nfs-provisioner-clusterRole
rules:
  - apiGroups: [""] # rules on persistentvolumes
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-provisioner-rolebinding
subjects:
  - kind: ServiceAccount
    name: nfs-pod-provisioner-sa
    namespace: default
roleRef: # binding cluster role to service account
  kind: ClusterRole
  name: nfs-provisioner-clusterRole # name defined in clusterRole
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-pod-provisioner-otherRoles
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-pod-provisioner-otherRoles
subjects:
  - kind: ServiceAccount
    name: nfs-pod-provisioner-sa # same as top of the file
    # replace with namespace where provisioner is deployed
    namespace: default
roleRef:
  kind: Role
  name: nfs-pod-provisioner-otherRoles
  apiGroup: rbac.authorization.k8s.io

 위의 sa.yaml 파일을 생성하였다면, 아래와 같이 k8s에 적용시켜줍니다.

kubectl create -f sa.yaml

 

  다음으로, ServiceAccount의 권한을 얻은 NFS 파일을 Dynamic Provisioning할수 있는 Pod를 Deployment 를 통해서 띄워줍니다. (배포)

 

 이 provisioner.yaml 파일은 아래와 같이 작성합니다. 

kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-pod-provisioner
spec:
  selector:
    matchLabels:
      app: nfs-pod-provisioner
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-pod-provisioner
    spec:
      serviceAccountName: nfs-pod-provisioner-sa # name of service account
      containers:
        - name: nfs-pod-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-provisioner
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME # do not change
              value: <임의의 StorageClass 이름> # SAME AS PROVISIONER NAME VALUE IN STORAGECLASS
            - name: NFS_SERVER # do not change
              value: <NFS 폴더가 있는 서버의 IP> # Ip of the NFS SERVER
            - name: NFS_PATH # do not change
              value: <NFS 폴더 위치 in 서버> # path to nfs directory setup
      volumes:
       - name: nfs-provisioner # same as volumemouts name
         nfs:
           server: <NFS 폴더가 있는 서버의 IP>
           path: <NFS 폴더 위치 in 서버>

  위의 provisioner.yaml 파일을 생성하였다면, 아래와 같이 k8s에 적용시켜줍니다.

kubectl create -f provisioner.yaml

  이제 provisioner 가 동적으로 PV를 생성하고자 할 때 사용하게 될 StorageClass를 생성해야 합니다. 

 

아래와 같이 nfs_storage.yaml을 생성합니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <사용하고 싶은 StorageClass 이름> # 아래의 이름과 달라도 됨
spec:
  storageClassName: <임의의 StorageClass 이름> # SAME NAME AS THE STORAGECLASS
  accessModes:
    - ReadWriteMany #  must be the same as PersistentVolume
  resources:
    requests:
      storage: 1Gi

 위의 nfs_storage.yaml 파일을 생성하였다면, 아래와 같이 k8s에 적용시켜줍니다.

kubectl create -f nfs_storage.yaml

 마지막으로 nfs_pvc.yaml 을 아래와 같이만들어 줍니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <pvc 이름 자유롭게>
spec:
  storageClassName: <위에서 정의한 StorageClass 이름> # SAME NAME AS THE STORAGECLASS
  accessModes:
    - ReadWriteMany #  must be the same as PersistentVolume
  resources:
    requests:
      storage: 1Gi

위의 nfs_pvc.yaml 파일을 생성하였다면, 아래와 같이 k8s에 적용시켜줍니다.

kubectl create -f nfs_pvc.yaml

여기까지 진행한 후에, kubectl get pv,pvc를 통해 확인하였을 때 아래와 같이 연결되었다면 제대로 dyanmic provisioning이 이루어진 것입니다. 

 

 

 이때, 최신 k8s 버전에서는 위와같이 바로 Bound가 되지 않을 수 있습니다. 이때는 아래의 수정 사항을 반영해주시고 다시 create 들을 시행해주시면 됩니다.

 

  Master Node에서 kube-apiserver.yaml에 아래 옵션을 추가 [5]

$ vi /etc/kubernetes/manifests/kube-apiserver.yaml

- --feature-gates=RemoveSelfLink=false


 처음 메일로 질문을 주신분이 있어서 답변을 남기고자 합니다. 위의 방식은 data & log server의 특정 폴더를 persistent volume으로 사용하는 방법이 아니라 "provisioner.yaml"에서 정의한 path 에 해당하는 폴더 위치에 k8s에서 pv를 요청할 때마다 pv를 생성합니다. 

 

 예를들면, path를 /mnt/ts_logs 라고 설정하였다면 pv 요청이 들어올 때마다 /mnt/ts_logs/temp_1, /mnt/ts_logs/temp_2 와 같이 새로운 폴더가 생성되고 해당 폴더에 container가 접근하게 된다고 보시면 될 것 같습니다.

 

 만약, /mnt/ts_logs를 직접적으로 nfs 방식으로 container에 연결하고 싶다면 해당 폴더를 persistent volume으로 정으하는 pv.yaml 을 아래와 같이 정의하고 pvc를 해당 pv 를 가리키도록 하면 됩니다. 

apiVersion: v1
kind: PersistentVolume
metadata:
  name: <임의의 이름>
spec:
  capacity:
    storage: 2Gi                         # 사용할 용량을 2GB로 설정
  accessModes:                           # Storage 특정 접근 모드 선택
     - ReadWriteMany
  storageClassName: <storage class 이름 설정; 다른 pv와 구분되도록>
  persistentVolumeReclaimPolicy: Retain  # 사용이 종료되면 볼륨 삭제
  nfs:
    path: <mount 할 폴더 위치>
    server: <mount 할 폴더가 있는 서버의 IP>
    readOnly: true

 

 

References

[1] 52. Kubernetes Volume [Dynamic Provisioning] (tistory.com)

 

52. Kubernetes Volume [Dynamic Provisioning]

Kubernetes Volume [Dynamic Provisioning] [kubernetes volume 관련 글 목록] Kubernetes Volume Object 개념 Kubernetes Volume [Static Provisioning] Kubernetes Volume [Dynamic Provisioning] Kube..

ikcoo.tistory.com

[2] 09. Kubernetes Volume 개념 (tistory.com)

 

09. Kubernetes Volume 개념

Kubernetes Volume Object 개념 [kubernetes volume 관련 글 목록] Kubernetes Volume Object 개념 Kubernetes Volume [Static Provisioning] Kubernetes Volume [Dynamic Provisioning] Kubernetes Sta..

ikcoo.tistory.com

[3] 쿠버네티스 #5 - 디스크 (볼륨/Volume) (tistory.com)

 

쿠버네티스 #5 - 디스크 (볼륨/Volume)

쿠버네티스 #4 Volume (디스크) 조대협 (http://bcho.tistory.com) 이번 글에서는 쿠버네티스의 디스크 서비스인 볼륨에 대해서 알아보도록 하겠다. 쿠버네티스에서 볼륨이란 Pod에 종속되는 디스크이다. (

bcho.tistory.com

[4] K8S 보안 "인증/인가" (tistory.com)

 

K8S 보안 "인증/인가"

개요 Kubernetes는 인증(Authentication)과 인가(Authorization)를 통해 보안을 관리할 수 있다. 인증은 User에 대한 접속 허가 여부를 결정하는 방식으로 일반적인 사용자의 ID/Password 기반 로그인을 의미한다

waspro.tistory.com

[5] https://happycloud-lee.tistory.com/178

 

NFS서버 설치와 NFS Dynamic Provisioning 설정

NFS서버 설치는 아래 글을 참고하세요 . happycloud-lee.tistory.com/46?category=832247 NFS서버 만들기 1. ubuntu https://hiondal.blog.me/221624709742 NFS서버 만들기 k8s에서 Volume으로 사용할 수 있는 종..

happycloud-lee.tistory.com