Docker

kubenetes - 퍼시스턴트 볼륨(PV)과 퍼시스턴트 볼륨 클레임(PVC)

쿠와와 2021. 2. 19. 18:23

지금까지는 디플로인먼트의 각 포드는 별도의 데이터를 가지고 있지 않았으며, 단순히 요청에 대한 응답만 반환했다. 하지만 데이터베이스처럼 포드 내부에서 특정 데이터를 보유해야 하는 상태가 있는 애플리케이션의 경우에는 데이터를 어떻게 관리할지 고민해야한다.

즉 포드의 데이터를 영속적으로 저장하기 위한 방법이 필요하다. (docker의 볼륨) 
쿠버네티스에서는 호스트에 위치한 디렉토리를 각 포드와 공유함으로써 데이터를 보전하는 것이 가능하지만 한계가 존재한다. 바로 포드에 장애가 생겨 다른 노드로 옮겨갔을 때 해당 데이터를 사용할 수 없는 문제점이다. 

 

그래서 필요한 방법이 퍼시스턴트 볼륨을 사용하는 것이다. 

 

개념 

워커 노드들이 네트워크상에서 스토리지를 마운트해 영속적으로 데이터를 저장할 수 있는 볼륨을 의미 

 

종류

- NFS

- AWS의 EBS

- Ceph

- GlusterFS

- 쿠버네티스에서 자체적으로 제공해주는 PV, PVC 

 

우리는 이중에서 PV, PVC를 공부할 것이다. 

 

1. 로컬 볼륨: hostPath, emptyDir

먼저 쿠버네티스에서 간단히 사용해 볼 수 있는 hostPath, emptyDir 두 종류의 볼륨을 알아보자.

hostPath: 호스트와 볼륨을 공유

emptyDir: 컨테이너 간에 볼륨을 공유

 

이 두개는 잘 사용되지는 않지만 간단하니깐 익숙해지게 사용해보자 

 

hostPath

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-pod
spec:
  containers:
    - name: my-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: my-hostpath-volume
        mountPath: /etc/data
  volumes:
    - name: my-hostpath-volume
      hostPath:
        path: /tmp

 

위의 코드는 포드를 생성한 뒤 내부로 들어가 etc/data 디렉토리에 파일을 생성하면 호스트의 /tmp 디렉터리에 파일이 저장되는 것이다.

즉, 포드 의 etc/data와 호스트의 /tmp는 동일한 디렉터리로써 사용되는 것이다. 

 

호스트의 tmp 디렉터리 위치

 

 

호스트의 쿠버네티스 포드위치에서 아래의 명령어 사용

kubectl apply -f hostpath-pod.yaml
pod/hostpath-pod created


kubectl exec -i -t hostpath-pod -- touch /etc/data/mydata

 

워커

 

하지만 이러한 방식의 데이터의 보존은 바람은 바람직하지 않다. 아까말했듯 포드에 장애가 생겨 다른 노드로 포드가 옮겨져갔을 경우 이전 노드에 저장된 데이터를 사용할 수 없기 때문이다. 

 

특수한 경우(모든 워커 노드에 배포해야하는 데이터들)를 제외하면 보안 및 활용성 면에서 그다지 좋지 않기 때문에 신중히 고려해서 사용하자. 

 

emptyDir

이 볼륨은 포드의 데이터를 영속적으로 보존하기 위해 외부 볼륨을 사용하는 것이 아닌 휘발성 데이터를 각 컨테이너가 함께 사용할 수 있도록 임시 저장하는 공간을 생성한다. 

즉 포드가 삭제되면 저장되어 있던 데이터도 함께 삭제된다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: emptydir-pod
spec:
  containers:
  - name: content-creator
    image: alicek106/alpine-wget:latest
    args: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: my-emptydir-volume
      mountPath: /data                      # 1. 이 컨테이너가 /data 에 파일을 생성하면

  - name: apache-webserver
    image: httpd:2
    volumeMounts:
    - name: my-emptydir-volume
      mountPath: /usr/local/apache2/htdocs/  # 2. 아파치 웹 서버에서 접근 가능합니다.

  volumes:
    - name: my-emptydir-volume
      emptyDir: {}                             # 포드 내에서 파일을 공유하는 emptyDir

empthDir은 한 컨테이너가 파일을 관리하고 한 컨테이너가 그 파일을 사용하는 경우에 유용하게 사용할 수 있다. content-create 컨테이너 내부로 들어가 /data 디렉터리에 웹 컨텐츠를 생성하면 아파치 웹 서버 컨테이너의 htdocs 디렉터리에도 동일하게 웹 컨텐츠 파일이 생성될 것이고, 이로서 웹 서버에 의해 외부에 제공된다.

kubectl apply -f emptydir-pod.yaml
kubectl exec -it emptydir-pod -- -c content-creator sh

 

2. 네트워크 볼륨

쿠버네티스에서는 별도의 플러그인이 없어도 다양한 클라우드 플랫폼의 볼륨을 포드에 마운트 할 수 있다. 

네트워크 볼륨의 위치는 정해진 것은 없으며, 네트워크로 접근 할 수 있다면 쿠버네티스 클러스터 내부, 외부 어느 곳에 존재해도 크게 상관은 없다. (AWS의 EBS 제외) 

 

우리는 간단히 사용할 수 있는 NFS 볼륨의 사용법을 알아보자. 다른 것들은 공식문서를 참고하자. 

네트워크 볼륨의 종류는 매우 많다. 일반적으로 선택 기준은 데이터의 읽기 및 쓰기 속도, 마운트 방식(1:1 or 1:N) 네트워크 볼륨 솔류션 구축 비용등을 고려한다. 각자에 맞는 솔루션을 찾아서 사용하도록 하자. 

 

NFS는 영속적인 데이터가 실제로 저장되는 네트워크 스로리지 서버이다. 이것만 별도로 구축하면 다로 뭘 준비할 필요는 없다. 

별도의 튜닝이 적용된 NFS 서버를 구축하는 것이 좋으나 지금은 간단하게 사용하기 위해 임시 NFS 서버를 하나 생성해보겠다. 

nfs-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-server
spec:
  selector:
    matchLabels:
      role: nfs-server
  template:
    metadata:
      labels:
        role: nfs-server
    spec:
      containers:
      - name: nfs-server
        image: gcr.io/google_containers/volume-nfs:0.8
        ports:
          - name: nfs
            containerPort: 2049
          - name: mountd
            containerPort: 20048
          - name: rpcbind
            containerPort: 111
        securityContext:
          privileged: true

 

nfs-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: nfs-service
spec:
  ports:
  - name: nfs
    port: 2049
  - name: mountd
    port: 20048
  - name: rpcbind
    port: 111
  selector:
    role: nfs-server

NFS 서버를 위한 디플로이먼트와 서비스 

 

 

nfs-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nfs-pod
spec:
  containers:
    - name: nfs-mount-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: nfs-volume
        mountPath: /mnt           # 포드 컨테이너 내부의 /mnt 디렉터리에 마운트합니다.
  volumes:
  - name : nfs-volume
    nfs:                            # NFS 서버의 볼륨을 포드의 컨테이너에 마운트합니다.
      path: /
      server: {NFS_SERVICE_IP}		# 이 부분 조심 원래 NFS 서비스 IP 넣어줘야합니다.

NFS 서버를 컨테이너에 마운트하는 포드를 생성

 

호스트

kubectl apply -f nfs-deployment.yaml 
kubectl apply -f nfs-service.yaml

 

mountPath를 /mnt로 설정했기 때문에 NFS 서버의 네트워크 볼륨은 포드 컨테이너의 /mnt 디렉터리에 마운트 될 것

 

volumes 에서 NFS를 사용한다고 명시

NFS 볼륨의 마운트는 컨테이너 내부가 아닌 워커 노드에서 발생하므로 서비스의 DNS 이름으로 NFS에 접근할 수 없다. 노드에서는 포드의 IP로 통신할 수 있지만 쿠버네티스의 DNS를 사용하도록 설정돼 있지는 않기 때문이다. 

 

이번에는 예외적으로 NFS 서비스의 Cluster IP를 직접얻은 뒤, YAML 파일에 사용하는 방식으로 포드를 생성해보겠다. 

# NFS 서버에 접근하기 위한 서비스 클러스터 아이피 얻기
export NFS_CLUSTER_IP=$(kubectl get svc/nfs-service -o jsonpath='{.spec.clusterIP}')

# nfs-pod의 server 항목을 클러스터 아이피로 교체해 생성
cat nfs-pod.yaml | sed "s/{NFS_SERVICE_IP}/$NFS_CLUSTER_IP/g" | kubectl apply -f -

그 후 제대로 마운트 했는지 확인해보자 

kubectl get pod nfs-pod
kubectl exec -it nfs-pod -- sh


# 혹시 안된다면
kubectl describe pods 명령어로 무엇이 문제인지 파악하고 포드가 할당된 워커 노드에서 아래의 명령어로 NFS 관련된 패키지를 설치하자
apt-get install nfs-common

 

3. PV, PVC를 이용한 볼륨 관리

쿠버네티스에서 지원하는 대부분의 볼륨 타입은 포드나 디플로이먼트 YAMl 파일에서 직접 정의해 사용한다. 실제로 애플리케이션을 개발한 뒤 YAML 파일로 배포할 때는 문제가 생긴다. 

 

 

직접 명시를 하게되면 볼륨과 애플리케이션의 정의가 서로 밀접하게 연관돼 있어 서로 분리할 수 없는 상황이 돼 버린다.

이러한 불편함을 해결하기 위해 쿠버네티스에서는 퍼시스턴트 볼륨(PV)과 퍼시스턴트 볼륨 클레임(PVC) 이 두 오브젝트를 제공한다. 이것은 포드가 볼륨의 세부적인 사항을 몰라도 볼륨을 사용할 수 있게 추상화해주는 역활을 한다. 즉, 포드를 생성하는 YAML입장에서는 네트워크 볼륨이 NFS인지 EBS인지 상관없이 볼륨을 사용할 수있다.

 

 

흐름도

1. 인프라 관리자는 네트워크 볼륨의 정보를 이용해 퍼스스턴트 볼륨 리소스를 미리 생성해둔다. 네트워크 볼륨의 정보에는 NFS나 iSCSI와 같은 스토리지 서버에 마운트하기 위한 엔드포인트가 포함될 수 있다. 

 

2. 사용자는 포드를 정의하는 YAML 파일에 '이 포드는 데이터를 영속히 저장해야 하므로 마운트할 수 있는 외부 볼륨이 필요'라는 의미의 퍼시스턴트 볼륨 클레임을 명시하고, 해당 퍼시스턴트 볼륨 클레임을 생성

 

3. 쿠버네티스는 기존에 인프라 관리자가 생성해 뒀던 퍼스스턴트 볼륨의 속성과 사용자가 요청한 퍼시스턴트 볼륨 클레임의 요구사항이 일치한다면 두 개의 리소스를 매칭 시켜 바인드한다. 포드가 이 퍼시스턴트 볼륨 클레임을 사용함으로써 포드의 컨테이너 내부에 볼륨이 마운트 된 상태로 생성

 

=> 사용자는 디플로이먼트의 YAML 파일에 볼륨의 상세한 스펙을 정의하지 않아도 된다. 볼륨의 사용 여부만 남겨주면 된다. 

 

 

사용해보기

앱을 배포하려는 개발자의 입장에서 사용해보자.

퍼시스턴트 볼륨 : persistentvolumne -> pv

퍼시스턴트 볼륨 클레임: persistentvolumnclaim -> pvc

 

이번에는 ebs에서 퍼시스턴트 볼륨을 사용해보겠다.

5기가의 EBS 만들기 -> 아마존에서 제공하는 것이기 때문에 다른 곳에서는 안됨 ex. GKE

export VOLUME_ID = $(aws ec2 create-volume --size 5 \
--region ap-northeast-2 \
--availability-zone ap-northeast-2a \
--volume-type gp2
--tag-specifications \
'ResoutceType=volume,Tags=[{Key=KubernetesCluster,Value=mycluster.k8s.local}]' \
| jp '.VolumeId' -r)

 

ebs-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: ebs-pv
spec:
  capacity:
    storage: 5Gi         # 이 볼륨의 크기는 5G입니다.
  accessModes:
    - ReadWriteOnce    # 하나의 포드 (또는 인스턴스) 에 의해서만 마운트 될 수 있습니다.
  awsElasticBlockStore:
    fsType: ext4
    volumeID: <VOLUME_ID>

 

 

위의 YAML 파일에서는 스토리지는 볼륨의 크기를 ReadWriteonce는 해방 볼륨이 1:1로 마운트 가능함을 의미 

 

위에서 EBS를 생성할 때 셸 변수에 저장했던 VOLUME_ID 값을 이용해 pv를 생성해보겠다. 

cat ebs-pv.yaml | set "s/<VOLUME_ID>$VOLUME_ID/g" | kubectl apply -f -

그 후 확인해바

 

kubectl get pv

 

이벤에는 애플리케이션을 뱊포학려는 사용자의 입장이 되어 퍼시스턴트 볼륨 클레임과 포드를 함께 생성해 보겠다.

ebs-pod-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-ebs-pvc                  # 1. my-ebs-pvc라는 이름의 pvc 를 생성합니다.
spec:
  storageClassName: ""
  accessModes:
    - ReadWriteOnce       # 2.1 속성이 ReadWriteOnce인 퍼시스턴트 볼륨과 연결합니다.
  resources:
    requests:
      storage: 5Gi          # 2.2 볼륨 크기가 최소 5Gi인 퍼시스턴트 볼륨과 연결합니다.
---
apiVersion: v1
kind: Pod
metadata:
  name: ebs-mount-container
spec:
  containers:
    - name: ebs-mount-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: ebs-volume
        mountPath: /mnt
  volumes:
  - name : ebs-volume
    persistentVolumeClaim:
      claimName: my-ebs-pvc    # 3. my-ebs-pvc라는 이름의 pvc를 사용합니다.

 

 

볼륨 클레인을 정의하는 YAML파일이 accessModes와 resources 항목은 봉륨의 요구사항으로, 해당 조건을 만족하는 퍼시스턴트 볼륨과 연결돼야 한다는 의미이다. 

위의 조건들은 앞에 생성해 둔 EBS의 퍼시스턴트 볼륨과 일치하므로 문제없이 바인드 된 뒤 포드에 마운트 될 것

 

위의 파일을 이용해 포드와 pvc를 생성해보자

kubectl apply -f ebs-pod-pvc.yaml

kubectl get pv,pvc

kubectl get pods

 

pv, pvc의 클레임 상태가 bound로 설정 되었다면 두 리소스가 성공적으로 연결 된 것이다. 

 

pv : 네임스페이스에 속하지 않은 클러스터 단위의 오브젝트 

pvc : 네임스페이스에 속하는 오브젝트 

 

pv을 선택하기 위한 조건 명시

 

처음에 말했듯이 실제로 볼륨이 어떠한 스펙을 가졌는지 알 필요가 없지만, 사용하려는 볼륨이 애플리케이션에 필요한 최소한의 조건을 맞출 필요는 있을 것이다. 

ex. 

볼륨의 크기가 적어도 얼마나 돼야 하는지

여러 개의 포드에 의해 마운트 될 수 있는지

읽기 전용으로만 사용할 수 있는지 등등 

-> AccessMode 나 볼륨의 크기 등이 바로 이러한 조건, 쿠버네트시는 두 리소스를 매칭해 바인드함 

 

accessmodes와 볼륨 크기, 스토리지클래스, 라벨 셀렉터를 이용한 퍼시스턴트 볼륨 선택

 

accessmodes 종류

accessmodes 이름 kubectl get에서 출력되는 이름 속성 설명
ReadWriteOnce RWO 1:1 마운트, 읽기 쓰기 가능
ReadOnlyMany ROX 1:N 마운트, 읽기 전용
ReadWriteMany
RWX 1:N 마운트, 읽기 쓰기 기능

 

볼륨의 크기, 스토리지 클래스, 라벨 셀렉터도 많이 본다.
위에 pv.yaml과 pvc.yaml 을 보면 두개의 storage가 같은 것을 볼 수 있다. 

pv.yaml pvc.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: ebs-pv
spec:
  capacity:
    storage: 5Gi        
  accessModes:
    - ReadWriteOnce
  storageClassName: my-ebs-volume
  awsElasticBlockStore:
    fsType: ext4
    volumeID: <VOLUME_ID> 
    # volumeID: vol-0390f3a601e58ce9b


apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-ebs-pvc-custom-sc
spec:
  storageClassName: my-ebs-volume
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: ebs-mount-container-custom-sc
spec:
  containers:
    - name: ebs-mount-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: ebs-volume
        mountPath: /mnt
  volumes:
  - name : ebs-volume
    persistentVolumeClaim:
      claimName: my-ebs-pvc-custom-sc
   
apiVersion: v1
kind: PersistentVolume
metadata:
  name: ebs-pv-label
  labels:
    region: ap-northeast-2a
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  awsElasticBlockStore:
    fsType: ext4
    # volumeID: vol-025c52fbd39d35417
    volumeID: <여러분의 VOLUME ID를 입력합니다> 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-ebs-pvc-selector
spec:
  selector:
    matchLabels:
      region: ap-northeast-2a
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: ebs-mount-container-label
spec:
  containers:
    - name: ebs-mount-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: ebs-volume
        mountPath: /mnt
  volumes:
  - name : ebs-volume
    persistentVolumeClaim:
      claimName: my-ebs-pvc-selector

 

 

pv의 라이프 사이클과 reclaim policy

포드와 퍼시스턴트 볼륨 클레임을 삭제하면 퍼시스턴트 볼륨의 상태가 Available이 아닌 Released 상태로 변경이 된다. 이것은 볼륨의 사용이 끝이 났다는 것을 위미하며 Released 상태에 있는 pv는 다시 사용할 수 없다. 그렇지만 실제 데이터는 볼륨 안에 고스란히 보존 돼 있기 때문에 퍼시스턴트 볼륨을 삭제한 뒤 다시 생성하면 Available 상태의 볼륨을 다시 사용할 수 있다. 

 

 

퍼시스턴트 볼륨 클레임을 삭제했을 때 퍼시스턴트 볼룸의 데이터를 어떻게 처리할 것인지 별도로 지정할 수도 있다. 

쿠버네티스에서는 이를 Reclaim Policy라고 하며 크게 종류로는 Retain, Delete, Recycle 방식이 있다. 

 

Retain : default 값으로 볼륨의 사용이 끝난 뒤에도 원격 스토리지에 저장된 데이터를 계속해서 보존하고 싶을 때 사용 이때의 라이프사이클은 Available-> Bound -> Released가 된다.

 

Delete : pv사용이 끝난 뒤에 자동으로 pv를 삭제하며 가능한 경우에 한해서 연결된 외부 스토리지도 함께 삭제된다. 

 

Recycle : 퍼시스턴트 볼륨 클레임이 삭제 됐을 때 퍼시스턴트 볼륨의 데이터를 모두 삭제한 뒤 Available 상태로 만들어 주는 Recycle이라는 정책을 사용할 수도 있다. -> 하지만 이제는 사용되지 않을 기능이다. 

 

 

다이나믹 프로비저닝과 스토리지 클래스 

매번 볼륨 스토리지를 직접 수동으로 생성하고 스토리지에 대한 접근 정보를 yaml 파일에 적는 것은 비효율적이기 때문에 쿠버네티스는 다이나믹 프로비저닝이라는 기능을 제공한다.

 

다이나믹 프ㄹ비저닝 : pvc가 요구하는 조건과 일치하는 pv이 존재하지 않는다면 자동으로 퍼시스턴트 볼륨과 외부 스토리지를 함께 프로비저닝하는 기능 

 

스토리지 클래스는 다이나믹 프로비저닝에도 사용이 되며 클래스의 정보를 참고해 외부 스토리지를 생성한다. 

 

 

1. fast라는 스토리지 클래스에는 SDD를 생성하라는 설정, slow에는 HDD를 생성하라는 설정

2. pvc에 특정 스토리지 클래스를 명시해 생성 -> 조건과 일치하는 pv가 존재하지 않다고 가정

3. 조건에 일치하는 퍼시스턴트 볼륨을 새롭게 만들기 위해 스토리지 클래스에 정의된 속성에 따라서 외부 스토리지를 생성 

4. 새롭게 생성된 외부 스토리지는 쿠버네티스의 퍼시스턴트 볼륨으로 등록되거, 퍼시스턴트 볼륨 클레임과 바인딩 됨.

 

단, 다이나믹 프로비저닝을 모든 쿠버네티스 클러스터에서 사용할 수 있는 것은 아니며, 지원 되는 스토리지 프로비저너가 미리 활성화돼 있어야 한다. (AWS, GCP 같은 클라우드 플랫폼에서는 별도로 설정하지 않아도 된다.)