본문 바로가기
프로젝트 일지/BoWS

Synology NAS NFS 기반 PV 생성하기

by 데브겸 2024. 8. 17.

지금까지는 emptyDir을 바탕으로 DB의 데이터를 저장하고 있었다. 하지만 emptyDir은 파드의 라이프사이클과 똑같기 때문에 파드가 다시 뜨게 되면 기존에 저장되어 있는 데이터가 모두 날라가게 된다. 이를 방지하기 위해 emptyDir이 아니라 파드가 띄워진 노드의 파일시스템에 접근하는 hostPath나 외부에 데이터를 영구적으로 저장하는 PV 등을 이용한다. 띄워지는 서비스들의 데이터를 영구적으로 보관하기 위해서 PV를 선택했는데 이유는 아래와 같다.

  • hostPath를 이용하지 않은 이유
    • k8s 노드들을 띄운 물리적 서버는 컴퓨터 1대이고 저장용량이 그렇게 크지 않다(1T 이상이긴 하지만 k8s 노드용 vm외 다른 vm, 다른 프로그램들도 돌고 있으니)
    • 어떤 데이터를 노드에 저장한다는 것은 pod가 특정 노드에 종속된다는 의미이다. Node Affinity를 주면 되기야 하지만 하나인 여러 node들을 추상화하여 하나의 노드 위에 있는 것처럼 사용한다는 k8s의 장점을 적극적으로 이용하지 못한다는 생각이 들었다
  • NAS를 사용한 이유
    • 사용할 수 있는 Synology NAS가 집에 있었다… (thanks to 아버지…) 심지어 NAS 용량도 꽤 많이 남아돌고 있는 상태이고, 써도 된다는 허락도 있었다
    • k8s 위 서비스들의 데이터를 영구화하기 위해 외부 서비스(EBS 등)를 사용하는 경우가 많은데 이를 유사하게 구현할 수 있다

여튼 위와 같은 이유로 NAS의 NFS 기능을 사용하여 서비스들에서 저장되는 데이터를 영구화하기로 하였다.

 

 

Synology NAS NFS 기능 사용하기

Admin 권한이 있어야 NFS 모드를 켤 수 있기 때문에 해당 권한이 있는 계정으로 접속하자.

 

나의 경우 비밀번호 초기화부터 했다. NAS 뒷편에 초기화 구멍에 펜심 등을 이용해 4초 눌러 어드민 계정을 초기화할 수 있다. 어드민 초기화 때 NAS에 저장된 데이터에는 영향이 가지 않지만 혹시 모르니 백업을 해두자. 초기화 방법은 아래 유튜브를 참고했다.

https://www.youtube.com/watch?v=JyJB6kmNIME

 

 

권한이 있는 계정으로 들어오고 제어판의 ‘공유 폴더’를 클릭하여 NFS에 사용할 폴더를 만든다.

 

 

글을 좀 찾아보니 Proxmox에서 NFS에서 4.1을 지원해주는 것 같아 최대 프로토콜을 4.1로 설정. (다만 QNAP의 NFS 4.1과는 조금 문제가 있어보이니 잘 알아보자)

 

 

생성 이후 ‘제어판’ → 원하는 공유폴더 편집 → ‘NFS 규칙 생성’에서 설정. 나는 내부 네트워크에서만 사용할 것이기 때문에 IP를 아래와 같이 사용했다.

 

 

 

 

하단에 적힌 ‘마운트 경로’는 쓸 일이 있으므로 메모해두자.

 

이제 NFS를 사용할 k8s 노드들에 nfs 마운트 명령어 수행을 위한 nfs-common을 설치해준다

sudo apt-get install -y nfs-common

 

애플리케이션 생성때 동적으로 PV를 생성하기 위해 NFS StorageClass를 사용한다(언제, 얼마만큼의 PV를 사용하는 프로젝트가 생성될지 모르기 때문에). 다만, k8s에는 내장 NFS 프로비저너가 없기 때문에 가장 유명한 nfs-subdir-external-provisioner 를 사용한다.

 

GitHub - kubernetes-sigs/nfs-subdir-external-provisioner: Dynamic sub-dir volume provisioner on a remote NFS server.

Dynamic sub-dir volume provisioner on a remote NFS server. - kubernetes-sigs/nfs-subdir-external-provisioner

github.com

 

 

nfs-subdir-external-provisioner를 helm install하면 storageClass, clusterRole, clusterRoleBinding 생성을 다 처리해줘서 편하다. nfs.path에 위에 ‘마운트 경로’에서 봤던 그 path를 적어준다.

https://kubernetes.io/ko/docs/concepts/storage/storage-classes/#nfs

$ helm repo add nfs-subdir-external-provisioner <https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/>
$ helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \\
    --set nfs.server=x.x.x.x \\
    --set nfs.path=/exported/path

 

이렇게 StorageClass를 만들어주면, StorageClass가 그 내부에서 provisioner와 provisioner에 전달할 parameter, volumeBindingMode 등을 관리한다. provisioner는 이전에 미리 등록한 스토리지 정보 등을 들고 있어서 요청이 들어왔을 때 PVC요청에 맞춰 storageClass 내용을 토대로 PV 생성, PVC에 바인딩한다.

 

기존에 hostPath로 처리해줬던 DB manifesto를 StatefulSet으로 바꿔준다. 하나의 프로젝트에 하나의 DB만 있는 상황이지만, 고유한 이름을 만들어주기도 하고, PVC를 별도로 만들지 않고 volumeClaimTemplates로 관리할 수 있다.

 

기존

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.projectName | lower }}-db
  namespace: {{ .Values.namespace }}
  labels:
    projectName: {{ .Values.projectName | lower }}
    tier: db
spec:
  replicas: 1
  selector:
    matchLabels:
      projectName: {{ .Values.projectName | lower }}
      tier: db
  template:
    metadata:
      labels:
        projectName: {{ .Values.projectName | lower }}
        tier: db
    spec:
      containers:
      - name: {{ .Values.projectName | lower }}-db
        image: {{ .Values.app.db.image.name }}:{{ .Values.app.db.image.tag }}
        ports:
        - containerPort: {{ .Values.app.db.port }}
        env:
          {{- range $name, $value := .Values.app.db.env }}
          - name: {{ $name }}
            value: {{ $value }}
          {{- end }}
        volumeMounts:
          - name:  {{ .Values.projectName | lower }}-storage
            mountPath: /var/lib/{{ .Values.projectName | lower }}/storage
      volumes:
      - name: {{ .Values.projectName | lower }}-storage
        emptyDir: {}

---
apiVersion: v1
kind: Service
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Values.projectName | lower }}-db-service
spec:
  selector:
    projectName: {{ .Values.projectName | lower }}
    tier: db
  ports:
    - protocol: {{ .Values.app.db.protocol }}
      port: {{ .Values.app.db.port }}
      targetPort: {{ .Values.app.db.port }}

 

변경 후

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Values.projectName | lower }}-db
  namespace: {{ .Values.namespace }}
  labels:
    projectName: {{ .Values.projectName | lower }}
    tier: db
spec:
  serviceName: {{ .Values.projectName | lower }}-db-service
  replicas: 1
  selector:
    matchLabels:
      projectName: {{ .Values.projectName | lower }}
      tier: db
  template:
    metadata:
      labels:
        projectName: {{ .Values.projectName | lower }}
        tier: db
    spec:
      containers:
      - name: {{ .Values.projectName | lower }}-db
        image: {{ .Values.app.db.image.name }}:{{ .Values.app.db.image.tag }}
        ports:
        - containerPort: {{ .Values.app.db.port }}
        env:
          {{- range $name, $value := .Values.app.db.env }}
          - name: {{ $name }}
            value: {{ $value | quote }}
          {{- end }}
        volumeMounts:
          - name: {{ .Values.projectName | lower }}-storage
            mountPath: /var/lib/mysql
            subPath: {{ .Values.projectName | lower }}
  volumeClaimTemplates:
  - metadata:
      name: {{ .Values.projectName | lower }}-storage
      namespace: {{ .Values.namespace }}
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: nfs-client
      resources:
        requests:
          storage: {{ .Values.app.db.storage.size }}

---
apiVersion: v1
kind: Service
metadata:
  namespace: {{ .Values.namespace }}
  name: {{ .Values.projectName | lower }}-db-service
spec:
  selector:
    projectName: {{ .Values.projectName | lower }}
    tier: db
  ports:
    - protocol: {{ .Values.app.db.protocol }}
      port: {{ .Values.app.db.port }}
      targetPort: {{ .Values.app.db.port }}

 

 

PVC Manifesto에 persistentVolumeReclaimPolicy가 없어 기본 설정(Delete)으로 되는 것을 볼 수 있는데, 이렇게 해도 데이터는 안전하게 보관된다. nfs-client의 parameter 중 archiveOnDelete 옵션이 true로 되어있어 데이터가 보관해주기 때문

Name Description Default
onDelete If it exists and has a delete value, delete the directory, if it exists and has a retain value, save the directory. will be archived with name on the share: archived-<volume.Name>
archiveOnDelete If it exists and has a false value, delete the directory. if onDelete exists, archiveOnDelete will be ignored. will be archived with name on the share: archived-<volume.Name>
pathPattern Specifies a template for creating a directory path via PVC metadata's such as labels, annotations, name or namespace. To specify metadata use ${.PVC.}. Example: If folder should be named like -, use ${.PVC.namespace}-${.PVC.name} as pathPattern. n/a

 

accessModes의 경우 file storage형태를 사용할 때 주로 ReadWriteMany 옵션을 사용한다고 하지만, 현재 내가 만든 chart에서는 db에 접근하는 백엔드 앱은 1개이므로 ReadWriteOnce 옵션을 넣어주었다.

 

 

깨알 트러블슈팅

emtyDir를 썼을 때 아래와 같이 mountPath를 적어도 아무 이상없었으나, pv를 사용하기 시작하면서 DB 커넥션이 안 되는 에러 발생

volumeMounts:
  - name: {{ .Values.projectName | lower }}-storage
    mountPath: /var/lib/{{ .Values.projectName | lower }}/storage

 

나의 추측으로는 원래 mysql은 기본으로 /var/lib/mysql을 사용한다는 점을 고려했을 때 엉뚱한 path에 mount했을 가능성이 크다. 하지만 어짜피 emptyDir로 설정했으니까 파드가 살았때만 유효하고, 죽었을 때 없어지니까 티가 안 났던 것 같다. 어디에 마운트를 하든 어짜피 Pod내에서 지지고 볶아서 상관없다.

 

하지만 PV, Statefulset 기반으로 바꾸면서 초기화 데이터 등이 모두 /var/lib/mysql에 쌓이는데 PV는 /var/lib/{{ .Values.projectName | lower }}/storage 와 연결되어 있을 것이다. 둘 간의 간극이 생겨 에러가 발생했을 가능성이 높아보인다.

 

 

여튼, 이제 PV를 설정한 애플리케이션을 띄워보자. 애플리케이션을 띄우고 데이터를 저장한 뒤 helm uninstall 혹은 k delete -f로 서비스를 죽인다.

 

 

서비스를 다시 살리고 나도 데이터가 지워지지 않은 것을 확인할 수 있다.