Post

쿠버네티스 컨테이너 스토리지

쿠버네티스 컨테이너 스토리지

쿠버네티스 컨테이너에서 스토리지란?

쿠버네티스에서 파드(Pod) 속 컨테이너의 생애 주기는 매우 유동이다 컨테이너에 문제가 생겨 충돌이 발생하면 쿠버네티스는 해당 컨테이너를 종료하고 새로운 컨테이너를 시작하여 파드를 복구하는데 이때 가장 큰 문제는 데이터의 휘발성 즉, 데이터가 사라진다

파드가 재시작되거나, 다른 노드로 교체(스케줄링)되면 데이터는 보존되지 않고 초기 상태로 돌아간다 애플리케이션의 중요한 데이터를 잃지 않으려면, 파드의 생애 주기와 독립적으로 데이터를 저장할 수 있는 볼륨(Volume)이 필요하다

 

emptyDir: 파드와 생애 주기를 함께하는 임시 볼륨

emptyDir 볼륨은 파드가 생성될 때 만들어지는 임시 디렉터리 이름처럼 처음에는 비어있는 상태로 시작

  • 특징: 파드와 동일한 생애 주기를 갖는다
  • 장점: 파드 내의 여러 컨테이너가 동일한 emptyDir 볼륨을 마운트하여 파일을 공유할 수 있다 컨테이너가 재시작되더라도 파드가 살아있는 동안에는 데이터가 유지
  • 단점: 파드가 삭제되거나 다른 노드로 재스케줄링되면 emptyDir의 모든 데이터는 영구적으로 사라진다

 

실습: 데이터 유지 및 소멸 과정 확인

1. emptyDir를 사용하는 파드 생성

아래 YAML 파일을 emptydir-pod.yaml로 저장 이 파드는 emptyDir 볼륨(cache-volume)을 생성하고, 컨테이너의 /cache 경로에 마운트한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# emptydir-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-emptydir
spec:
  containers:
  - name: container-1
    image: kiamol/ch03-sleep
    volumeMounts:
    - name: cache-volume
      mountPath: /cache
  volumes:
  - name: cache-volume
    emptyDir: {}

1
kubectl apply -f emptydir-pod.yaml #배포

2. 볼륨에 파일 생성 및 컨테이너 강제 종료

파드 내부로 들어가 마운트된 경로에 파일을 생성

1
2
3
4
5
6
7
8
9
10
11
# 파드 내부 셸로 접속
kubectl exec -it test-emptydir -- /bin/sh

# /cache 디렉터리로 이동하여 파일 생성
cd /cache
echo "Hello from container 1" > test.txt
cat test.txt
Hello from container 1

# 셸 종료
exit

이제 컨테이너를 강제로 종료하여 쿠버네티스가 컨테이너를 재시작

1
kubectl exec -it test-emptydir -- /bin/kill 1

위 과정으로 보면 컨테이너를 삭제해도 해당 텍스트 문서는 유지되었다

3. 파드 삭제 후 데이터 소멸 확인

이제 파드를 삭제하고 다시 생성

1
2
kubectl delete pod test-emptydir
kubectl apply -f emptydir-pod.yaml

새로 생성된 파드에 접속하여 /cache 디렉터리를 확인하면, 이전에 만들었던 test.txt 파일이 사라지고 다시 비어있는 디렉터리가 된 것을 볼 수 있다

1
2
3
4
5
kubectl exec -it test-emptydir -- /bin/sh

cd cache
ls
# 비어있음

이처럼 emptyDir는 파드의 생애 주기에 완전히 종속된다

 

hostPath: 노드에 데이터를 고정하는 볼륨

파드가 사라져도 데이터를 보존하기 위한 가장 간단한 방법은 파드가 실행 중인 노드(Node)의 파일 시스템에 데이터를 직접 저장 이를 hostPath 볼륨이라고 한다

  • 특징: 노드의 특정 디렉터리를 컨테이너에 마운트

  • 장점: 파드가 삭제되고 동일한 노드에 다시 생성되어도 데이터가 유지

  • 단점:

    • 노드 종속성: 파드가 다른 노드에 생성되면 해당 데이터에 접근할 수 없다 이는 2개 이상의 노드를 사용하는 클러스터에서 심각한 문제를 일으킬 수 있음
    • 보안 취약점: 컨테이너가 노드의 파일 시스템에 직접 접근할 수 있게 되므로, 악의적인 컨테이너가 호스트 시스템을 공격하는 통로가 될 수 있음

📖 hostPath 볼륨 사용 예시

아래는 노드의 /mnt/data 디렉터리를 파드의 /data 경로에 마운트하는 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# hostpath-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-hostpath
spec:
  containers:
  - name: test-container
    image: kiamol/ch03-sleep
    volumeMounts:
    - name: host-storage
      mountPath: /data
  volumes:
  - name: host-storage
    hostPath:
      # 호스트 노드의 디렉터리 경로
      path: /mnt/data
      # 디렉터리가 없으면 생성하도록 지정
      type: DirectoryOrCreate

컨테이너의 /data경로로 이동하면 노드의 /mnt/data에 마운트됨(노드 디렉토리가 보임)

 

🔒 subPath로 보안 강화하기

hostPath의 보안 문제를 완화하기 위해 subPath를 사용할 수 있다 subPath는 볼륨 전체가 아닌, 볼륨 내의 특정 하위 디렉터리나 파일만 컨테이너에 마운트하는 기능 이를 통해 컨테이너가 노드의 파일 시스템에 필요 이상으로 노출되는 것을 방지할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# hostpath-subpath-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-hostpath-subpath
spec:
  containers:
  - name: test-container
    image: kiamol/ch03-sleep
    volumeMounts:
    - name: host-storage
      mountPath: /data/pod-specific-data # 컨테이너의 마운트 경로
      subPath: my-pod-data # 볼륨 내에서 사용할 하위 디렉터리 이름
  volumes:
  - name: host-storage
    hostPath:
      path: /mnt/shared-data
      type: DirectoryOrCreate

위 예시에서 컨테이너는 호스트의 /mnt/shared-data 디렉터리 전체가 아닌, 그 안의 my-pod-data라는 하위 디렉터리에만 접근할 수 있음

 

영구 볼륨(PV)과 클레임(PVC): 노드에 독립적인 스토리지

hostPath의 가장 큰 문제인 ‘노드 종속성’을 해결하려면 클러스터의 모든 노드에서 접근 가능한 스토리지가 필요 이를 위해 분산 스토리지 시스템을 사용한다

쿠버네티스는 이러한 외부 스토리지를 파드에 연결하기 위해 영구 볼륨(PersistentVolume, PV)영구 볼륨 클레임(PersistentVolumeClaim, PVC)이라는 추상화된 리소스를 제공한다

  • 분산 스토리지 시스템 (NFS, Ceph 등): 데이터와 처리 작업을 ‘여러 대’의 독립적인 서버(노드)에 나누어 저장하고 관리 이 서버들은 네트워크로 연결되어 사용자에게는 마치 하나의 거대한 스토리지 시스템처럼 보인다 NFS, Ceph, GlusterFS 등이 대표적인 예
    • 관리자가 준비해 둔 아주 큰 피자 한 판 (예: 10TB 용량의 Ceph 클러스터)
  • 영구 볼륨 (PersistentVolume, PV): 클러스터 관리자가 프로비저닝한 실제 스토리지 조각 PV는 NFS, AWS EBS, Azure Disk 등 실제 스토리지의 종류, 용량, 접근 모드 등의 정보를 담고 있다 PV는 클러스터 전체에서 사용 가능한 리소스
    • 관리자가 이 피자 한 판에서 미리 잘라 놓은 한 조각 PV를 만들 때 용량이나 접근 모드 등을 정의 (예: “이 조각은 10GB 크기야”, “저 조각은 50GB 크기야”)
  • 영구 볼륨 클레임 (PersistentVolumeClaim, PVC): 개발자(사용자)가 파드에서 필요한 스토리지에 대한 요청서 “나는 50Mi 용량의 스토리지가 필요하고, 한 번에 하나의 파드에서만 읽고 쓸 수 있어야 한다”와 같은 요구사항을 정의 파드는 PV를 직접 사용하지 않고, PVC를 통해 스토리지 사용을 요청한다
    • 사용자가 “피자 한 조각 주세요!“라고 요청하는 것 이 요청서에는 “저는 10GB 정도 크기의 조각이 필요해요”와 같은 요구사항

쿠버네티스는 PVC의 요구사항(용량, 접근 모드 등)을 만족하는 PV를 찾아 1:1로 바인딩(연결)해준다 개발자는 복잡한 스토리지 인프라를 몰라도 PVC만 정의하여 파드에 연결하면 영구적인 스토리지를 사용할 수 있음

 

📖 로컬 스토리지를 이용한 PV/PVC 실습 (정적 프로비저닝)

분산 스토리지가 없는 환경을 가정하고, 특정 노드의 로컬 디스크를 영구 볼륨으로 만들어 사용하는 방법을 실습

1. 노드에 레이블 부여

로컬 볼륨은 특정 노드에 존재하므로, PV가 올바른 노드를 찾아가고, 파드가 해당 PV를 사용하는 노드에 스케줄링되도록 레이블(Label)을 사용해야 한다

먼저, 스토리지를 할당할 노드를 정하고 해당 노드에 레이블을 부여

1
2
# <your-node-name>을 실제 노드 이름으로 변경
kubectl label nodes <your-node-name> disktype=local-storage

레이블이 잘 부여되었는지 확인

1
kubectl get nodes --show-labels

2. 로컬 볼륨을 사용하는 영구 볼륨(PV) 생성

이제 레이블이 부여된 노드의 특정 경로(/mnt/local-pv)를 가리키는 PV를 생성 nodeAffinity를 사용하여 disktype=local-storage 레이블이 있는 노드에만 이 PV가 속하도록 강제한다

아래 내용을 local-pv.yaml로 저장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# local-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv01
spec:
  capacity:
    storage: 50Mi
  accessModes:
    - ReadWriteOnce # 하나의 파드에서만 읽기/쓰기 가능
  storageClassName: "manual" # 동적 프로비저닝을 방지하기 위해 클래스 이름 지정
  local:
    path: /mnt/local-pv # 노드의 실제 스토리지 경로
  nodeAffinity: # 하위 부분으로 해당 노드를 찾는다
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: disktype # 키 값
          operator: In
          values: # 벨류 우리가 레이블을 생성할때 disktype=local-storage 이렇게 만듦
          - local-storage

PV를 생성

1
kubectl apply -f local-pv.yaml

생성된 PV의 상태가 Available(사용 가능)인지 확인

1
kubectl get pv local-pv01

3. 영구 볼륨 클레임(PVC) 생성

이제 위에서 만든 PV를 사용하겠다는 요청서(PVC)를 작성한다 PVC는 accessModesresources.requests.storage가 일치하고, storageClassName이 동일한 PV를 찾아 바인딩을 시도한다

아래 내용을 local-pvc.yaml로 저장

1
2
3
4
5
6
7
8
9
10
11
12
# local-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-local-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Mi
  storageClassName: "manual" # 위 PV와 동일한 클래스 이름을 지정

PVC를 생성

1
kubectl apply -f local-pvc.yaml

잠시 후 PVC와 PV의 상태를 확인하면 둘 다 Bound(연결됨) 상태로 변경됨

1
kubectl get pv,pvc

4. PVC를 사용하는 파드 배포

마지막으로, 생성한 PVC를 볼륨으로 사용하는 파드를 배포

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# pvc-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pvc-pod
spec:
  containers:
    - name: my-container
      image: kiamol/ch03-sleep
      volumeMounts:
        - name: my-storage
          mountPath: /data
  volumes:
    - name: my-storage
      persistentVolumeClaim:
        claimName: my-local-pvc # 위에서 생성한 PVC 이름

파드를 생성하면, 쿠버네티스는 이 파드를 PVC가 바인딩된 PV가 위치한 노드(즉, disktype=local-storage 레이블이 붙은 노드)에 자동으로 스케줄링한다 이제 이 파드의 /data 디렉터리에 저장되는 파일은 파드가 삭제되어도 노드의 /mnt/local-pv에 영구적으로 보존된다

하지만 현재 마스터 노드, 워커 노드 2개가 존재해서 pod가 워커 노드에 생성되면 마스터 노드에 존재하는 pv,pvc에 연결할 수 없다 따라서 master노드에 pod를 실행해야한다

1
Warning  FailedScheduling  2m12s  default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) had volume node affinity conflict. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.# 에러 메세지 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# pvc-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pvc-pod
spec:
  # -------------------- [추가된 부분 1] --------------------
  nodeSelector:
    node-role.kubernetes.io/control-plane: ""
  # ---------------------------------------------------------

  # -------------------- [추가된 부분 2] --------------------
  tolerations:
  - key: "node-role.kubernetes.io/control-plane"
    operator: "Exists"
    effect: "NoSchedule"
  # ---------------------------------------------------------
    
  containers:
    - name: my-container
      image: kiamol/ch03-sleep
      volumeMounts:
        - name: my-storage
          mountPath: /data
  volumes:
    - name: my-storage
      persistentVolumeClaim:
        claimName: my-local-pvc # 위에서 생성한 PVC 이름

해당 pod에서 데이터를 저장하고 pod를 삭제해도 노드에 생성한 pv에 데이터가 저장된것을 확인할 수 있다

 

스토리지 클래스와 동적 볼륨 프로비저닝

지금까지의 방식은 관리자가 수동으로 PV를 생성하고, 사용자가 PVC를 생성하여 연결하는 정적 볼륨 프로비저닝(Static Volume Provisioning)이다 이 방식은 모든 쿠버네티스 클러스터에서 동작하며 스토리지 관리가 엄격한 환경에 적합

하지만 대부분의 클라우드 기반 쿠버네티스 플랫폼(GKE, EKS, AKS 등)에서는 훨씬 간단한 동적 볼륨 프로비저닝(Dynamic Volume Provisioning)을 제공한다

  • 동적 프로비저닝: 사용자가 PVC만 생성하면, 클러스터가 PVC의 요구사항에 맞는 PV를 자동으로 생성하고 바인딩해주는 방식

이것은 StorageClass라는 리소스를 통해 가능 StorageClass는 “어떤 종류의 스토리지를 동적으로 생성할 것인가”에 대한 정책을 정의한다 예를 들어, gp2라는 AWS EBS 볼륨 타입을 사용하는 StorageClass를 만들어두면, 사용자가 이 클래스를 지정하여 PVC를 요청할 때마다 쿠버네티스는 AWS에 요청하여 새로운 EBS 볼륨을 생성하고 이를 PV로 만들어 PVC와 연결해준다

클라우드 서비스(GKE, AWS 등)를 사용하면 기본 스토리지 클래스가 이미 만들어져 있는 경우가 많아서, 개발자가 따로 만들지 않고 바로 사용할 수도 있다

개발자는 더 이상 PV의 존재를 신경 쓸 필요 없이, 필요한 스토리지의 종류(storageClassName)와 용량만 명시하여 PVC를 생성하면 즉시 영구 스토리지를 사용할 수 있다 이것이 현대 쿠버네티스 환경에서 가장 널리 사용되는 방식

This post is licensed under CC BY 4.0 by the author.