Profile picture

[k8s] Deep Dive - Volume

JaehyoJJAng2023년 04월 04일

▶︎ Storage Volume

쿠버네티스는 Storage Volune을 정의하는 방법으로 이 기능을 제공하고 있다. Storage Volume은 Pod와 같은 최상위 리소스는 아니지만 Pod의 일부분으로 정의되며 Pod와 동일한 lifecycle을 가진다.

즉, Pod가 시작되면 Volume이 생성되고, 파드가 삭제되면 볼륨이 삭제된다는 뜻이다.

이를 통해 생성되는 새로운 컨테이너는 이전 컨테이너가 볼륨에 기록한 모든 파일들을 열람할 수 있다. 또한 파드가 여러 개의 컨테이너를 가진 경우 모든 컨테이너가 볼륨을 공유할 수 있게된다.


▶︎ 요약

  • 쿠버네티스 Volume은 Pod의 구성 요소로 컨테이너와 동일하게 Pod Spec에서 정의된다.
  • 볼륨은 쿠버네티스 오브젝트가 아니므로 자체적으로 생성, 삭제될 수 없다.
  • 볼륨은 Pod의 모든 컨테이너에서 사용 가능하지만 접근하려는 컨테이너에서 각각 마운트 되어야 한다. 각 컨테이너에서 파이시스템의 어느 경로에나 볼륨을 마운트할 수 있다.

▶︎ 로컬 볼륨

자중 사용되는 Volume 종류는 아님.

  • emptyDir: ephemeral Data를 저장하는데 사용되는 간단한 empty 디렉터리이다.
  • hostPath: Node 파일시스템을 Pod의 디렉터리로 마운트하는데 사용한다.

image


‣ emptyDir

image


• 특징

  • Pod의 컨테이너 간 Volume을 공유하기 위해 사용하며 컨테이너 간 파일을 공유할 때 유용
  • Pod가 실행되는 도중에만 필요한 휘발성 데이터를 각 컨테이너가 함께 사용할 수 있도록 임시 저장 공간을 생성함

• 공유 시나리오

  • emptydir-pod YAML 파일 생성 (컨테이너 1, 컨테이너 2)
  • 컨테이너 1 (content-creator)에 /data 디렉토리에 test.html 작성
  • 컨테이너 2 (apache-webserver) 에서의 /usr/local/apache2/htdocs/ 디렉토리와 컨테이너 1의 /data Volume과 공유된다.

즉, 컨테이너 2의 /usr/local/apache2/htdocs/ 에 test.html 이 저장되었다고 볼 수 있으며 Pod IP 주소를 호출시 /htdocs/test.html 파일을 불러들인다.

emptyDir-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: emptydir-pod
spec:
  containers:

# 첫 컨테이너는 content-creator 이라고 이름 짓고 alicek106/alpine-wget:latest 이미지를 실행
  - name: content-creator
    image: alicek106/alpine-wget:latest
    args: ["tail", "-f", "/dev/null"]

# my-emptydir-volume 이라는 볼륨을 컨테이너의 /data 에 마운트한다
# 마운트 = 디스크 파티션을 특정한 위치(디렉토리)에 연결시켜주는 과정
    volumeMounts:
    - name: my-emptydir-volume
      mountPath: /data                      # 1. 이 컨테이너가 /data 에 파일을 생성하면

# my-emptydir-volume이라는 볼륨을 컨테이너의 /usr/local/apache2/htdocs/에 마운트한다
  - name: apache-webserver
    image: httpd:2
    volumeMounts:
    - name: my-emptydir-volume
      mountPath: /usr/local/apache2/htdocs/  # 2. 아파치 웹 서버에서 접근 가능합니다.

# my-emptydir-volume 이란 단일 볼륨을 위의 컨테이너 2개에 마운트한다.
  volumes:
    - name: my-emptydir-volume
      emptyDir: {}                             # 포드 내에서 파일을 공유하는 emptyDir

• 공유 테스트

$ kubectl apply -f emptydir-pod.yaml
pod/emptydir-pod created

# content-creater 컨테이너에 접속하여 test.html 작성
$ kubectl exec -it emptydir-pod -c content-creator sh
# echo Hello, Kubernetes! >> /data/test.html

# emptydir-pod IP 주소 확인
$ kubectl describe pod emptydir-pod | grep IP
              cni.projectcalico.org/podIP: 172.16.197.4/32
              cni.projectcalico.org/podIPs: 172.16.197.4/32
IP:           172.16.197.4


# 임시 Pod를 생성하여 정상적으로 emptydir Volume이 공유되고 있는지 확인 
$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl --restart=Never -- curl 172.16.197.4/test.html
Hello, Kubernetes!

‣ HostPath

image


• 특징

gitRepo나 emptyDir Volume은 Pod가 종료되면 데이터도 같이 삭제되면 반면 HostPath Volume은 그렇지 않다.
HostPath Volume은 호스트의 디렉토리(Node 단위)를 Pod와 공유하여 데이터를 저장한다.


hostpath-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-pod
spec:

# my-container이라는 이름의 컨테이너에 /etc/data에 my-hostpath-volume 의 볼륨을 마운트
  containers:
    - name: my-container
      image: busybox
      args: [ "tail", "-f", "/dev/null" ]
      volumeMounts:
      - name: my-hostpath-volume
        mountPath: /etc/data

# /tmp 경로에 마운트
  volumes:
    - name: my-hostpath-volume
      hostPath:
        path: /tmp

• 공유 테스트

# 컨테이너 內 /etc/data 디렉터리에 mydata 이름의 파일 생성
$ kubectl exec -it hostpath-pod -- touch /etc/data/mydata

# Pod가 생성된 Workernode로 접속
$ kubectl get po -o wide
NAME           READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
hostpath-pod   1/1     Running   0          17m   172.16.46.7   k8s-w2   <none>           <none>

# 노드에 파일 생성 확인
$ ls /tmp/mydata
/tmp/mydata

• 적합성

HostPath Volume은 데이터 저장소로 채택하기에는 적합하지 않음.

  • 특정 노드의 FileSystem에 저장되므로 파드가 다른 노드로 Scheduling이 되면 더 이상 이전 데이터륿 볼 수 없음.
    • k8s-node1 노드에 HostPath를 지정됐는데 파드가 k8s-node2에 스케줄링 되면 더 이상 이전 데이터를 불러올 수 없게 되는 것
  • Pod가 어떤 노드에 Scheduling 되느냐에 따라 민감하기 때문에 일반적인 Pod에 사용하는 것은 좋은 생각이 아니다. 특수한 경우를 제외한다면 hostPath를 사용하는 것은 보안 및 활용성 측면에서 그다지 바람직하지 않다.

▶︎ 네트워크 볼륨

네트워크 볼륨부터 **PersistentVolume(=PV)와 PersistentVolumeClaim(=PVC)**라는 개념이 존재한다.

  • Pod 내부에서 특정 데이터를 보유해야 하는 Stateful한(DB) Application 경우 Stateless한 (Pod, Deployment) 데이터를 영속적으로 저장하기 위한 방법이 필요하다.
  • Pod에서 실행중인 애플리케이션이 디스크에 데이터를 유지해야 하고 Pod가 다른 노드로 ReScheduling된 경우에도 동일한 데이터를 사용해야 한다면 Local Volume (emptyDir, HostPath Volume)을 사용할 수 없다.

어떤 클러스터 노드에서도 접근이 필요할 수 있기에 NAS 유형의 스토리지에 저장이 되어야만 함.

On-Premise 환경에서도 구축할 수 있는 NFS, ISCSI, GlusterFS와 같은 Network Volume뿐만 아니라 AWS의 EBS(Elastic Block Store) 등과 같은 클라우드 플랫폼의 Volume의 Pod에 마운트할 수 있다.


‣ NFS를 네트워크 볼륨으로 사용

Deployment 및 Service 생성

쿠버네티스 클러스터 내부에서 임시 NFS 서버를 생성하자.
NFS 기능을 간단히 이용해보기 위한 목적이므로 Deployment와 Service를 생성해주어야 한다.


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-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: nfs-service
spec:
  selector:
    role: nfs-server
  ports:
  - name: nfs
    port: 2049
  - name: mountd
    port: 20048
  - name: rpcbind
    port: 111
$ kubectl apply -f nfs-deployment.yaml
$ kubectl apply -f nfs-svc.yaml

‣ Pod 생성

Deployment와 Service를 통해 임시 NFS 서버를 생성하였다면 이제 NFS 서버의 볼륨을 Pod에서 마운트해보자.


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}

YAML 파일에서 유의해야할 부분은 {NFS_SERVICE_IP}이다.

  • NFS Volume의 마운트는 컨테이너 내부가 아닌 Worker Node에서 발생하기에 서비스의 DNS 이름으로 NFS 서버에 접근이 불가능하다.
  • 노드 내부에서는 Pod의 IP로 통신이 가능하지만, 쿠버네티스의 DNS 사용하도록 설정 되어있지는 않다.

방법은 여러가지가 있지만 여기서는 NFS 서비스의 ClusterIP를 직접 얻은 뒤, YAML 파일에 사용하는 방식으로 Pod를 생성해보겠다.

# NFS 서버에 접근하기 위한 서비스의 Cluster IP를 획득
$ export NFS_CLUSTER_IP=$(kubectl get svc/nfs-service -o jsonpath='{.spec.clusterIP}')

# nfs-pod의 Server 항목을 NFS_CLUSTER_IP로 교체하여 생성
$ cat ./nfs-pod.yaml | sed "s/{NFS_SERVICE_IP}/${NFS_CLUSTER_IP}/g" | kubectl apply -f -

💡 kubectl get 명령어의 -o jsonpath는 Resource의 특정 정보만 가져올 수 있는 편리한 옵션이다.


• NFS 마운트 테스트

NFS서버가 /mnt 디렉터리에 마운트 되어 있음을 확인할 수 있다.

네트워크로 접근할 수 있는 Volume인 NFS 서버에 파일이 영속적으로 저장되며 컨테이너 내부의 /mnt 디렉터리에 저장된 파일은 Pod가 다른 node로 옮겨지거나 Pod를 재시작해도 삭제되지 않는다.

# nfs-pod 접속
$ kubectl exec -it nfs-pod -- sh

/ # df -h
Filesystem                Size      Used Available Use% Mounted on
...
10.99.241.166:/          38.7G      4.0G     34.7G  10% /mnt
...

• 트러블슈팅

  • Error: NFS 파드 생성 중 ContainerCreating 상태에서 안 넘어가는 경우
$ kubectl get pod nfs-pod
NAME      READY   STATUS              RESTARTS   AGE
nfs-pod   0/1     ContainerCreating   0          16m

$ kubectl describe po  nfs-pod
Events:
  Type     Reason       Age                From               Message
  ----     ------       ----               ----               -------
  Normal   Scheduled    30s                default-scheduler  Successfully assigned default/nfs-pod to k8s-w2
  Warning  FailedMount  14s (x6 over 30s)  kubelet            MountVolume.SetUp failed for volume "nfs-volume" : mount failed: exit status 32

---

$ kubectl get po -o wide
NAME                        READY   STATUS              RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
nfs-pod                     0/1     ContainerCreating   0          9s    <none>         k8s-w2   <none>           <none>
nfs-server-ddf754fc-9vwsn   1/1     Running             0          55m   172.16.197.5   k8s-w3   <none>           <none>

# nfs-pod가 할당된 노드에 접속하여 NFS 패키지 설치
$ sudo apt-get install nfs-common -y

# Pod 재확인
$ kubectl get po -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
nfs-pod                     1/1     Running   0          79s   172.16.46.8    k8s-w2   <none>           <none>
nfs-server-ddf754fc-9vwsn   1/1     Running   0          57m   172.16.197.5   k8s-w3   <none>           <none>

Loading script...