동적 프로비저닝을 사용하면서 CSI 를 준수하는 벤더의 드라이버가 볼륨 resize 를 지원하는 경우에는 PVC 조정을 통해 볼륨 확장이 가능하다. 아래와 같이 실습을 진행해보자.

 

1. 스토리지 클래스 생성

다음과 같이 allowVolumeExpansion: true 를 지정하여 스토리지 클래스를 생성한다. 스토리지 클래스에 해당 항목을 지정하더라도, 실제 사용하는 볼륨 플러그인 드라이버 구현에서 지원해야 볼륨 확장이 가능한 점에 유의하자.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: sample-block-storageclass-resize
provisioner: local.csi.openebs.io
parameters:
  storage: "lvm"
  volgroup: "lvmvg"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

 

2. PVC 생성

PVC 는 특별한 설정이 필요 없다. Static NFS 는 이미 존재하는 디렉토리를 스토리지로 사용하고, k8s 가 용량을 제어하지 않기 때문에 확장이 불가능하여 Block 스토리지로 사용하였다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-block-pvc-resize
spec:
  storageClassName: sample-block-storageclass-resize
  volumeMode: Block
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi

 

3. Pod 생성

위 PVC 를 사용하는 파드를 생성하여 동적 프로비저닝이 일어나도록 한다. 블록 장치 연결 위치는 /dev/sample-block 이다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-block-pod-resize
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeDevices:
        - name: nginx-pvc
          devicePath: /dev/sample-block
  volumes:
    - name: nginx-pvc
      persistentVolumeClaim:
        claimName: sample-block-pvc-resize

 

4. 볼륨 크기 확인

동적 프로비저닝이 정상적으로 수행되었는지, 생성된 영구 볼륨의 크기는 몇인지, 실제 파드 연결된 볼륨의 크기는 몇인지 확인해본다.

$ kubectl get pvc sample-block-pvc-resize
NAME                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                       VOLUMEATTRIBUTESCLASS   AGE
sample-block-pvc-resize   Bound    pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4   500Mi      RWO            sample-block-storageclass-resize   <unset>                 115s

# PV 용량 확인
$ kubectl get pv pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                             STORAGECLASS                       VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4   500Mi      RWO            Delete           Bound    default/sample-block-pvc-resize   sample-block-storageclass-resize   <unset>                          9s

# 파드에 연결된 볼륨 용량 확인
$ kubectl exec -it sample-block-pod-resize -- blockdev --getsize64 /dev/sample-block
524288000

 

5. 볼륨 크기 확장

이제 아래처럼 PVC 매니페스트의 spec.resources.requests.storage 를 이전보다 더 크게 수정/적용한 후 볼륨 사이즈를 확인해본다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-block-pvc-resize
spec:
  storageClassName: sample-block-storageclass-resize
  volumeMode: Block
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 800Mi # 여기를 수정한 후, 다시 kubectl apply 한다.

apply 후 실제 적용은 최대 1분까지 소요된다.

$ kubectl apply -f sample-block-pvc-resize.yaml
persistentvolumeclaim/sample-block-pvc-resize configured

$ kubectl get pvc sample-block-pvc-resize
NAME                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                       VOLUMEATTRIBUTESCLASS   AGE
sample-block-pvc-resize   Bound    pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4   800Mi      RWO            sample-block-storageclass-resize   <unset>                 13m

$ kubectl get pv pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                             STORAGECLASS                       VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-2d5bf77e-956e-4e38-9517-bb8f4da44ca4   800Mi      RWO            Delete           Bound    default/sample-block-pvc-resize   sample-block-storageclass-resize   <unset>                          11m

$ kubectl exec -it sample-block-pod-resize -- blockdev --getsize64 /dev/sample-block
838860800

같은 디스크에서 용량만 800Mi 로 증가된 것을 확인할 수 있다.

 

단, 아래처럼 볼륨 사이즈 축소는 불가한 점에 유의하자.

# PVC 의 spec.resources.requests.storage 를 축소한 후 적용시
$ kubectl apply -f sample-block-pvc-resize.yaml
The PersistentVolumeClaim "sample-block-pvc-resize" is invalid: spec.resources.requests.storage: Forbidden: field can not be less than previous value

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

영구 볼륨을 일반적인 파일 시스템으로부터 어태치 하지 않고 블록 장치로부터 어태치할 수도 있다. PVC 의 spec.volumeMode: Block 으로 지정하고(기본값이 FileSystem 임), PVC 를 사용하는 Pod 에서는 컨테이너 템플릿의 volumeDevices 를 지정하면 된다.(파일시스템인 경우 volumeMounts 인 것과 대비됨)

 

단, 현재 로컬 머신에는 NFS 스토리지만 준비되어있기 때문에 블록 스토리지를 추가로 구성해줘야 한다. 블록 스토리지를 실습하기 위해서는 Longhorn을 사용하는 방법이 권장되지만, 맥북 리소스의 한계로 이전에 구축에 실패하였다. 따라서 이번에는 OpenEBS LocalPV-LVM/ZFS 방식으로 구축해본다. VM에 추가 가상 디스크를 구성하고, 각 노드에서 해당 디스크를 블록 장치로 사용하는 방식이다. 단, LocalPV 이기 때문에 클러스터 공유 스토리지가 아닌, 특정 노드의 로컬 디스크에 종속된 스토리지라서 노드 간에 데이터 공유가 안된다는 점이 유의하자.

 

1. 가상 머신에 블록 디스크 추가(모든 워커 노드)

각 워커 노드 머신의 전원을 종료(shutdown -h now)한 후, UTM 에서 디스크를 추가한다. 인터페이스는 VirtIO, 크기는 본인의 리소스를 고려하여 적절히 세팅한 후 저장한다.

이후 다시 워커 노드를 부팅 후, lsblk 명령어를 사용하여 추가된 디스크를 확인한다.

$ lsblk
NAME                      MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
...
vdb                       253:16   0    2G  0 disk

 

2. LVM(Logical Volume Manager)용 PV(Physical Volume) 생성(모든 워커 노드)

Logical Volume Manager 가 추가한 디스크를 사용할 수 있는 상태로 만드는 작업이다. 모든 워커 노드에서 아래 명령어를 입력한다.

# lvm2 도구 설치
$ sudo apt update && sudo apt install lvm2 -y

# 물리 볼륨 생성
$ sudo pvcreate /dev/vdb
  Physical volume "/dev/vdb" successfully created.

아래 명령어로 PV를 조회한다. 위에에서 생성한 /dev/vdb 가 조회되면 된다.

$ sudo pvs
  PV         VG        Fmt  Attr PSize   PFree
...
  /dev/vdb             lvm2 ---    2.00g  2.00g

 

3. VG(Volume Group) 생성(모든 워커 노드)

물리 볼륨(PV)들을 하나의 그룹으로 묶는다. VG 이름은 나중에 쿠버네티스 스토리지 클래스에서 그대로 사용하게 된다. 여기서는 lvmvg 이라고 지정했다. 모든 워커 노드에서 진행한다.

# /dev/vdb 물리 볼륨을 'lvmvg'라는 이름의 볼륨 그룹으로 생성한다.
$ sudo vgcreate lvmvg /dev/vdb
  Volume group "lvmvg" successfully created
  
# 조회
$ sudo vgs
  VG        #PV #LV #SN Attr   VSize   VFree
  lvmvg       1   0   0 wz--n-  <2.00g <2.00g

 

4. LVM-LocalPV CSI 드라이버 설치(마스터 노드)

OpenEBS 의 LVM 연동 도구를 설치한다. 마스터 노드에서 실행한다. openebs 네임스페이스가 아닌 kube-system 네임스페이스 설치가 기본값인 듯 하다.

# LVM-LocalPV 오퍼레이터(Operator)와 관련 구성 요소를 쿠버네티스 클러스터에 배포한다.
$ kubectl apply -f https://openebs.github.io/charts/lvm-operator.yaml

아래 커맨드를 실행하여 필요한 리소스들이 생성되었는지 확인한다.

$ kubectl -n kube-system get statefulsets -l openebs.io/component-name
NAME                     READY   AGE
openebs-lvm-controller   1/1     70m

$ kubectl -n kube-system get daemonsets -l openebs.io/component-name
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
openebs-lvm-node   2         2         2       2            2           <none>          70m

# openebs-lvm-controller 는 statefulset 이 관리하고, openebs-lvm-node 는 daemonset 이 관리한다.
$ kubectl -n kube-system get pods -l openebs.io/component-name
NAME                       READY   STATUS    RESTARTS   AGE
openebs-lvm-controller-0   5/5     Running   0          72m
openebs-lvm-node-8fmwx     2/2     Running   0          72m
openebs-lvm-node-dgjbp     2/2     Running   0          72m

 

5. 스토리지 클래스 생성

아래 매니페스트를 이용하여 SC를 생성한다. 프로비저너로 위에서 설치한 OpenEBS 의 CSI  드라이버를 사용하기 때문에 동적 프로비저닝 기능이 이미 포함되어있다. volumeBindingMode: WaitForFirstConsumer 를 주어서 실제 파드가 노드에 스케쥴링된 후에야 동적으로 볼륨 바인딩이 일어날 수 있도록 한다. 이는 LocalPV 를 사용할 때 중요하다. 스토리지가 노드에 종속되어있기 때문에 실제로 파드가 노드에 배정되었을 때 그 노드의 lvmvg 에서 공간을 쪼개 할당할 수 있기 때문이다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: sample-block-storageclass
provisioner: local.csi.openebs.io # 중요
parameters:
  storage: "lvm"
  volgroup: "lvmvg" # 미리 생성해두 Volume Group 명을 지정한다.
volumeBindingMode: WaitForFirstConsumer # 중요
allowVolumeExpansion: true

 

 

6. PVC 생성

아래 매니페스트를 이용하여 PVC를 생성한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-block-pvc
spec:
  storageClassName: sample-block-storageclass
  volumeMode: Block # 중요
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi

 

7. 파드 생성

실제로 블록 장치를 볼륨으로 사용할 파드를 배포한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-block-pod
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeDevices:
        - name: nginx-pvc
          devicePath: /dev/sample-block
  volumes:
    - name: nginx-pvc
      persistentVolumeClaim:
        claimName: sample-block-pvc

 

8. 동적 프로비저닝 확인

파드가 PVC 를 사용하는 시점에 동적으로 PV 가 생성되었는지 확인한다.

# PVC 가 BOUND 되었는지 확인
$ kubectl get pvc sample-block-pvc
NAME               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                VOLUMEATTRIBUTESCLASS   AGE
sample-block-pvc   Bound    pvc-560cb363-b43c-498c-b397-309cd893cc6c   500Mi      RWO            sample-block-storageclass   <unset>                 90m

# PV 가 생성되었는지 확인
$ kubectl get pv pvc-560cb363-b43c-498c-b397-309cd893cc6c
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS                VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-560cb363-b43c-498c-b397-309cd893cc6c   500Mi      RWO            Delete           Bound    default/sample-block-pvc   sample-block-storageclass   <unset>                          60m

# Pod 가 Running 중인지 확인
$ kubectl get pods sample-block-pod -o wide
NAME               READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
sample-block-pod   1/1     Running   0          90m   10.244.2.83   worker02   <none>           <none>

# Pod 내에 Block 장치가 연결되었는지 확인
$ kubectl exec -it sample-block-pod -- ls -l /dev/sample-block
brw------- 1 root root 252, 1 Feb 22 10:20 /dev/sample-block

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

동적 프로비저닝을 사용할 때 기본적으로 영구 볼륨 클레임 리소스를 생성하면 자동으로 영구 볼륨이 생성된다. 하지만, 실제 볼륨을 사용하고자 하는 파드에서 영구 볼륨 클레임을 지정하여 파드를 생성하기 전까지는 영구 볼륨이 생성된 채로 유지가 되어 낭비가 발생한다. 스토리지 클래스에는 이를 조정하는 volumeBindingMode 라는 항목이 있다. volumeBindingMode: WaitForFirstConsume 으로 지정하면 아래와 같이 동작한다.

  • 정적 프로비저닝시: PV는 Available(미리 생성하기 때문), PVC는 Pending 상태로 존재하다가, 파드에서 사용하는 시점에 바인딩됨
  • 동적 프로비저닝시: PV 는 없고, PVC는 Pending 상태로 존재하다가, 파드에서 사용하는 시점에 PV 생성 & 바인딩됨

즉, 실제 파드에서 영구 볼륨을 사용하는 시점에 영구 볼륨을 생성할 수가 있는 것이다. 이 항목에 올 수 있는 값은 아래와 같다.

volumeBindingMode 설정값 설명
Immediate 기본값. 즉시 영구 볼륨이 생성됨
WaitForFirstConsumer 처음으로 파드에 의해 사용될 때 영구 볼륨이 생성됨

 

아래와 같은 스토리지 클래스를 통해 PVC 를 생성한 후, 실제 파드에서 사용할 때까지 PV가 어떻게 동작하는지 살펴보자.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
# volumeBindingMode: WaitForFirstConsumer 인 스토리지 클래스는 위 매니페스트로 미리 생성해둔다.

# PV, PVC, POD 상태를 확인한다.
$ kubectl get pv
No resources found
$ kubectl get pvc
No resources found in default namespace.
$ kubectl get pods
NAME                                     READY   STATUS    RESTARTS   AGE
nfs-client-provisioner-cc5544bc4-v62cz   1/1     Running   0          2d3h

# PV 만 생성한 후, PVC 의 상태가 Pending이고, PV 가 아직 생성되지 않았음을 확인한다.
$ kubectl apply -f sample-nfs-provisioning-pvc.yaml
persistentvolumeclaim/sample-nfs-provisioning-pvc created
$ kubectl get pvc
NAME                          STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
sample-nfs-provisioning-pvc   Pending                                      nfs-client     <unset>                 4s
$ kubectl get pv
No resources found

# 해당 PVC 를 사용하는 파드를 띄운 후 PV 가 자동으로 생성 및 바인딩 되었는지 확인한다.
$ kubectl apply -f sample-nfs-provisioning-pod.yaml
pod/sample-nfs-provisioning-pod created
$ kubectl get pvc
NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
sample-nfs-provisioning-pvc   Bound    pvc-37ae35bc-517e-4c0c-a175-cc0aee0c6f44   200Mi      RWO            nfs-client     <unset>                 27s
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                 STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-37ae35bc-517e-4c0c-a175-cc0aee0c6f44   200Mi      RWO            Delete           Bound    default/sample-nfs-provisioning-pvc   nfs-client     <unset>                          7s

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

영구 볼륨을 미리 생성해두고, 영구 볼륨 클레임 요청에 따라 바인딩하여 사용하는 기본 방식에는 2가지 단점이 있다.

  • 사전에 영구 볼륨을 미리 생성해두어야 한다는 번거로움
  • 영구 볼륨 클레임이 요청하는 용량 이상의 영구 볼륨이 할당되어 사용되지 못하는 용량이 발생되는 문제

2번째 문제에 대해서, 영구 볼륨의 용량이 10Gi 이고, 영구 볼륨 클레임이 요청하는 용량이 5Gi 인 경우를 예를 들어보자.

쿠버네티스에서 PV 와 PVC 의 바인딩은 1:1 관계이다. 따라서 아래와 같은 특징이 있다.

  • 독점적 바인딩: PVC가 특정 PV와 연결(Bound)되면, 해당 PV 전체가 그 PVC의 소유가 됨
  • 분할 불가: 10GiB 용량의 PV 하나를 여러 개의 PVC(예: 5GiB + 5GiB)가 나누어 가질 수 없음

결과적으로, 5Gi 만 요청했더라도 바인딩 되는 순간 그 PV는 사용중 상태가 되어 다른 PVC 에 바인딩 되지 못하는 상태가 된다. 단, PVC의 5Gi 요청은 바인딩을 위한 "신청서" 일 뿐, 실제 볼륨의 물리적 크기를 제한하지는 않기 때문에, 파드에서는 실제로 10Gi 만큼의 공간을 다 쓸 수 있긴 하다.

 

여튼, 이러한 문제를 해결하는 것이 동적 프로비저닝(Dynamic Provisioning)이다. 동적 프로비저닝을 사용하면 영구 볼륨 클레임이 생성되는 시점에 동적으로 영구 볼륨을 생성하고 할당한다. 따라서 미리 영구 볼륨을 생성할 필요가 없고, 딱 필요한 용량만큼만 보유한 영구 볼륨을 생성할 수 있게 된다.

 

1. 동적 프로비저닝 설정

간단히 설명하면 스토리지 클래스의 provisioner 를 동적 프로비저너로 지정하고, 이를 영구 볼륨 클레임에서 사용하면 된다. 파드 매니페스트에 컨테이너 템플릿을 지정하듯이, 스토리지 클래스 매니페스트에 어떤 영구 볼륨을 동적으로 생성할지 정의한다.

퍼블릭 클라우드 프로바이더의 관리형 k8s 플랫폼에서는 기본적으로 동적 프로비저닝을 사용하는 스토리지 클래스가 생성되어 있지만, 지금처럼 따로 구축한 쿠버네티스 클러스터에서 static NFS 타입의 스토리지에서 동적 프로비저닝을 사용하기 위해서는 아래와 같이 진행한다.

 

1.1 RBAC 권한 설정

프로비저너가 PV를 생성/삭제할 수 있는 권한을 부여하기 위해 아래 매니페스트로 권한을 생성/할당한다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    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: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default # 프로비저너를 배포할 네임스페이스
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
$ kubectl get sa nfs-client-provisioner
NAME                     SECRETS   AGE
nfs-client-provisioner   0         77m

 

1.2 프로비저너를 Deployment 로 배포

아래 매니페스트를 이용하여 배포한다. 단, <NFS_SERVER_IP> 는 본인의 NFS 서버 IP로, <NFS_EXPORT_PATH> 는 본인의 NFS 서버 경로로 하여 환경변수를 세팅한다. 지정하는 serviceAccount 이름이 RBAC 의 serviceAccount 이름인 "nfs-client-provisioner" 와 일치해야 한다. "registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2" 버전은 본인이 사용하는 쿠버네티스 버전과 호환되는 버전을 사용한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER
              value: <NFS_SERVER_IP>
            - name: NFS_PATH
              value: <NFS_EXPORT_PATH>
      volumes:
        - name: nfs-client-root
          nfs:
            server: <NFS_SERVER_IP>
            path: <NFS_EXPORT_PATH>

 

$ kubectl get deployment nfs-client-provisioner
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   1/1     1            1           62m

 

1.3 스토리지 클래스 생성

아래 매니페스트로 스토리지 클래스를 생성해둔다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # 위 Deployment의 PROVISIONER_NAME과 일치해야 함
parameters:
  archiveOnDelete: "false"
reclaimPolicy: Delete
$ kubectl get sc nfs-client
NAME         PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  51m

 

1.4 PVC 생성하여 동적 프로비저닝 확인

아래 매니페스트처럼 "nfs-client" 스토리지 클래스를 지정한 PVC 를 생성한 후 동적 프로비저닝이 되었는지 확인한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-nfs-provisioning-pvc
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 200Mi
# PVC 확인
$ kubectl get pvc sample-nfs-provisioning-pvc
NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
sample-nfs-provisioning-pvc   Bound    pvc-49196cda-866a-4210-b7a7-7fad1e092a64   200Mi      RWO            nfs-client     <unset>                 48m

# 동적 프로비저닝된 PV 확인
$ kubectl get pv pvc-49196cda-866a-4210-b7a7-7fad1e092a64
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                 STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-49196cda-866a-4210-b7a7-7fad1e092a64   200Mi      RWO            Delete           Bound    default/sample-nfs-provisioning-pvc   nfs-client     <unset>                          49m

 

이렇게 PV 가 동적 프로비저닝 된 후에는, 수동으로 영구 볼륨을 생성한 경우와 동일하게 파드에서 사용하면 된다.

 

2. 동적 프로비저닝 된 PV 삭제

위에서 스토리지 클래스 설정에 reclaimPolicy: Delete 를 지정했기 때문에, PVC 를 삭제하여 바인딩이 해제되면 자동으로 PV가 삭제된다.

# PVC 삭제
$ kubectl delete -f sample-nfs-provisioning-pvc.yaml
persistentvolumeclaim "sample-nfs-provisioning-pvc" deleted

$ kubectl get pv
No resources found

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

볼륨영구 볼륨에 이어서 영구 볼륨 클레임(PersistentVolumeClaim, PVC)에 대해서 다룬다. 각각에 대한 짧은 정리는 아래와 같다.

  • 볼륨(Volume): 호스트에 미리 준비된 사용 가능한 볼륨을 일컬음. 쿠버네티스에서 볼륨 리소스를 생성/삭제할 수 없다. 매니페스트에 볼륨을 지정하기만 할 수 있음.
  • 영구 볼륨(PersistentVolume, PV): 외부의 볼륨 시스템에 연계하여 사용하는 볼륨. 쿠버네티스에서 볼륨 리소스를 생성/삭제한다. 영구 볼륨을 생성하면 클러스터에 등록만 된다.
  • 영구 볼륨 클레임(PersistentVolumeClaim, PVC): 이미 생성된 영구 볼륨을 사용 요청하는 리소스. 파드에서 영구 볼륨을 사용하기 위해 이것을 사용한다.

 

영구 볼륨 클레임은 영구 볼륨을 요청하는 리소스이다. 영구 볼륨 클레임에서는 영구 볼륨에 대해 요구하는 용량, 레이블 등을 지정한다. 파드에서 해당 영구 볼륨 클레임을 사용하면, 스케쥴러는 현재 클러스터에 존재하는 영구 볼륨 중에서 PVC의 조건을 만족하는 가장 적절한 영구 볼륨을 할당해준다.

 

1. 영구 볼륨 클레임 설정

PVC 에는 아래와 같은 항목들을 설정할 수 있다.

항목 설명
AccessModes 읽기/쓰기 권한
- RWO(ReadWriteOnce): 한 노드만 읽고 쓰기
- RWX(ReadWriteMany): 여러 노드가 읽고 쓰기
- ROX(ReadOnlyMany): 여러 노드가 읽기만
Resources 요청할 스토리지 용량(e.g. 1Gi)
StorageClassName 사용할 스토리지 클래스
Selector(Labels) 특정 라벨이 붙은 PV 를 찾기

이 항목들은 모두 영구 볼륨에도 정의된 값이며, PVC에서 요청하는 값에 부합하는 영구 볼륨이 할당된다. 주의할 점은 용량의 경우 PVC 에서 지정한 값이 최소 용량이 된다는 것이다. 1Gi 를 요청하더라도 1Gi 이상의 가장 작은 PV 가 할당된다.

 

2. 영구 볼륨 클레임 생성

아래와 같은 매니페스트를 이용하여 PVC를 생성한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc-nfs
spec:
  selector:
    matchLabels:
      type: nfs
      speed: high
    matchExpressions:
      - key: env
        operator: In
        values:
          - practice
  resources:
    requests:
      storage: 300Mi
  accessModes:
    - ReadWriteOnce
  storageClassName: manual

 

PVC 를 생성하면 자동으로 적절한 영구 볼륨을 확보한다.

# PV 확인. 현재 STATUS: Available
$ kubectl get pv
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
sample-pv-nfs   500Mi      RWO            Retain           Available           manual         <unset>                          4s

# PVC 생성
$ kubectl apply -f sample-pvc-nfs.yaml
persistentvolumeclaim/sample-pvc-nfs created

# PV 확인. STATUS: Bound 로 바뀜. default/sample-pvc-nfs 클레임이 할당됨.
$ kubectl get pv
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
sample-pv-nfs   500Mi      RWO            Retain           Bound    default/sample-pvc-nfs   manual         <unset>                          19s

# PVC 확인. STATUS: Bound
$ kubectl get pvc
NAME             STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
sample-pvc-nfs   Bound    sample-pv-nfs   500Mi      RWO            manual         <unset>                 10s

만약 조건을 만족하는 PV가 없다면 STATUS: Pending 으로 유지되며, PVC 를 describe 해보면 상세 내용을 확인할 수 있다.

 

3. 영구 볼륨 클레임 삭제

PV가 persistentVolumeReclaimPolicy: Retain 정책을 사용하고 있는 경우 PVC 가 삭제되면 바인딩 되었던 PV 는 Released 상태로 변경된다. Released 상태의 PV 는 다시 자동으로 PVC 에 할당될 수 없다.

# PVC 삭제
$ kubectl delete -f sample-pvc-nfs.yaml
persistentvolumeclaim "sample-pvc-nfs" deleted

# PV 확인. STATUS: Released
$ kubectl get pv
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
sample-pv-nfs   500Mi      RWO            Retain           Released   default/sample-pvc-nfs   manual         <unset>                          5d15h

 

4. 파드에서 PVC 를 사용하여 PV 마운트

이제 파드 매니페스트에서 spec.volumes[].persistentVolumeClaim 항목으로 아래와 같이 미리 생성해둔 PVC 를 지정한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-pvc-pod-nfs
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      ports:
        - containerPort: 80
          name: "http"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: nginx-pvc
  volumes:
    - name: nginx-pvc
      persistentVolumeClaim:
        claimName: sample-pvc-nfs

파드가 생성될 때 sample-pvc-nfs 라는 이름의 PVC 를 찾아 바인딩 된 PV 를 컨테이너의 "/usr/share/nginx/html" 경로로 마운트하게 된다.

이 샘플에서 sample-pvc-nfs PVC 가 바인딩한 PV 매니페스트는 아래와 같다. nfs 타입으로 master 노드의 "/srv/nfs/kubedata" 경로를 볼륨으로 사용했다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv-nfs
  labels: # 파드가 특정 PV를 찾을 수 있게 돕는 이름표
    type: nfs
    speed: high
    env: practice
spec:
  capacity:
    storage: 500Mi # 사용할 용량
  accessModes:
    - ReadWriteOnce
  storageClassName: manual # 수동으로 만든 PV임을 명시 (중요)
  nfs:
    server: 192.168.0.36 # 마스터 노드 IP 주소
    path: /srv/nfs/kubedata # 마스터 노드에서 만든 경로

 

이를 기반으로 실제 파드를 구동시켜보고, 파드 내 컨테이너 <-> master 노드 내 스토리지 간에 스토리지가 읽기/쓰기 공유되는지 아래 단계를 따라 확인해보자

# 파드 생성
$ kubectl apply -f sample-pvc-pod-nfs.yaml
pod/sample-pvc-pod-nfs created

# 파드가 Running 중임을 확인
$ kubectl get pods -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
sample-pvc-pod-nfs   1/1     Running   0          94s   10.244.1.85   worker01   <none>           <none>

# 파드 내 컨테이너에 접속하여 볼륨 마운트 경로에 존재하는 파일이 없음 확인
$ kubectl exec -it sample-pvc-pod-nfs -- /bin/bash
root@sample-pvc-pod-nfs:/# ls /usr/share/nginx/html

# 다른 SSH 터미널 세션을 열고 master 노드의 스토리지 경로 진입
$ cd /srv/nfs/kubedata

# 샘플 파일 생성
$ cat <<EOF > index.html
<h1>Hello, PVC from storage</h1>
EOF

# 컨테이너 내에서 볼륨 마운트 경로 다시 확인
root@sample-pvc-pod-nfs:/# cat /usr/share/nginx/html/index.html
<h1>Hello, PVC from storage</h1>

# 컨테이너 내에서 파일 수정
cat <<EOF > /usr/share/nginx/html/index.html
<h1>Hello, PVC from container</h1>
EOF

# master 노드에서 스토리지 내 파일 다시 확인
$ cat index.html
<h1>Hello, PVC from container</h1>

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

영구 볼륨(Persistent Volume)은 컨피그 & 스토리지 API가 아닌, 클러스터 API 카테고리에 해당하는 리소스이다. 별도의 매니페스트로 PV 리소스를 생성한 후 사용한다. 과거에는 영구 볼륨의 대표적인 플러그인 종류가 아래와 같이 다양하게 존재했지만, 현재는 CSI 방식으로 통일되었다.

  • GCE Persistent Disk
  • AWS Elastic Block Store
  • Azure File
  • nfs
  • iSCSI
  • Ceph(RBD, CephFS)
  • OpenStack Cinder
  • GlusterFS
  • Container Storage Interface(CSI)

자세한 것은 공식 문서 를 참고하며, 현재 표준인 Container Storage Interface 에 대해서만 좀 더 알아본다.

 

1. Container Storage Interface

초창기 kubernetes 는 Google, AWS, Azure 같이 유명한 kubernetes provider 에 대해서 각각의 스토리지 드라이버 소스코드를 kubernetes 소스코드 저장소에 함께 관리하고, 실제 쿠버네티스 바이너리에도 포함하여 배포하였다. 쿠버네티스 사용자가 별도의 벤더사 제공 드라이버 설치 없이도 바로 스토리지를 사용할 수 있다는 편리함이 있었지만, 아래와 같은 치명적인 단점이 존재했다.

  • 특정 벤더사의 스토리지 드라이버에 버그가 있더라도, 쿠버네티스 전체 코드를 함께 검토하고 버저닝을 해야 함
  • 수십개의 벤더사 코드를 포함하여 바이너리가 너무 무거워짐
  • 핵심 코드 내에 외부 벤더사의 코드가 섞여 보안 검증이 힘듦

이러한 단점들 때문에 Container Storage Interface 라는 영구 볼륨 플러그인이 생겼고, 현재는 이 CSI 의 표준에 맞추어 각 벤더사가 자체의 스토리지 드라이버를 개발하여 제공하는 형식이 되었다. 즉, 벤더사의 드라이버는 이제 스토리지 개발사가 직접 개발/관리하도록 된 것이다.

 

2. 볼륨으로 사용할 파일 시스템 구성하기(영구 디스크 설정)

퍼블릭 클라우드 프로바이더가 아닌 직접 로컬 PC에 구축한 k8s 환경을 기준으로 생성해본다. 퍼블릭 클라우드처럼 스토리지 클래스를 지정하면 자동으로 볼륨이 생성되는 "동적 프로비저닝"을 사용하기 위해서 Longhorn 이라는 오픈소스 분산 블록 스토리지 시스템을 사용한다. 이를 사용하면 여러 노드의 여유 디스크 공간을 논리적으로 묶어 하나의 Storage Pool로 만든다. 파드가 띄워진 노드가 변경되어도 변함없이 데이터에 접근이 가능하며, 복제, 백업, 스냅샷, 고가용성을 챙길 수 있다. 위에서 언급했듯이 쿠버네티스에서는 CSI 인터페이스를 통해서만 요청하며, 쿠버네티스 사용자가 Longhorn CSI 드라이버를 직접 설치하여야 한다.

 

2.1 Longhorn 으로 설치(리소스가 충분한 경우)

 

2.1.1 종속성 설치(모든 노드)

Longhorn 은 노드끼리 데이터를 주고받기 위해 NFS 와 iSCSI 프로토콜을 사용한다. control plane 을 포함한 모든 노드에서 아래 명령어를 실행한다.

$ sudo apt update
$ sudo apt install -y open-iscsi nfs-common

 

2.1.2 각 노드에서 iscsid 서비스가 실행중인지 확인

$ sudo systemctl enable --now iscsid

 

2.1.3 환경 체크(control-plane 에서만)

Longhorn 에서 제공하는 아래 환경 체크 스크립트를 실행한다. 에러가 나오지 않아야 한다.

$ curl -sSfL https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/scripts/environment_check.sh | bash

 

2.1.4 Longhorn 설치(control-plane 에서만)

$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml

 

2.1.5 설치 확인

아래 커맨드로 CSI 관련 파드들이 정상적으로 떴는지 확인한다. 모든 파드가 Running 상태가 될 때까지 대기한다.

$ kubectl get pods -n longhorn-system -w

 

2.1.6 대시보드 접속

아래 명령어로 port-forward 하여 브라우저에 접속할 수 있게 구성 후, http://<마스터노드-IP>:8080 접속해본다.

$ kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80 --address 0.0.0.0

 

2.1.7 StorageClass 확인

longhorn 네임스페이스에 StorageClass 가 생성되었는지 확인한다. 앞으로 PV 생성시 이 StorageClass 를 사용하면 된다.

$ kubectl -n longhorn get storageclasses
NAME                 PROVISIONER          RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
longhorn (default)   driver.longhorn.io   Delete          Immediate           true                   2m21s
longhorn-static      driver.longhorn.io   Delete          Immediate           true                   2m17s

 

2.1.8 동적 프로비저닝 테스트

(나는 여기까지 하다가 맥북의 메모리가 부족하여 NFS 방식으로 전환하였다.)

 

 

2.2 NFS(Network File System) 로 설치(리소스가 부족한 경우)

 

2.2.1 NFS 서버 설치(control-plane 에서만)

NFS 서비스 패키지를 설치하고, 공유할 디렉토리를 생성한다.

# 1. 패키지 설치
$ sudo apt update
$ sudo apt install -y nfs-kernel-server

# 2. 공유할 디렉토리 생성 (예: /srv/nfs/kubedata)
$ sudo mkdir -p /srv/nfs/kubedata
$ sudo chown nobody:nogroup /srv/nfs/kubedata
$ sudo chmod 777 /srv/nfs/kubedata

 

2.2.2 NFS 설정(control-plane 에서만)

공유를 허용할 네트워크 대역을 설정 파일에 등록한다.(실습환경이므로 * 으로 전체 허용)

우분투에서 /etc/exports 파일은 NFS 서버가 어떤 디렉터리를 어떤 클라이언트에게, 어떤 권한으로 공유할지 정의하는 핵심 설정 파일이다. 자세한건 검색해보자.

# 설정 파일 열기
$ sudo vi /etc/exports

# 아래 내용 작성
# 문법: <공유할_디렉터리> <클라이언트>(옵션)
# 예시: /srv/nfs 192.168.1.0/24(rw,sync,no_subtree_check)
/srv/nfs/kubedata *(rw,sync,no_subtree_check,no_root_squash)

 

2.2.3 NFS 서비스 적용(control-plane 에서만)

아래 커맨드로 NFS 설정을 활성화한다.

# 수정된 exports 설정 반영
$ sudo exportfs -ra

# NFS 서버 서비스 재시작 및 활성화
$ sudo systemctl restart nfs-kernel-server
$ sudo systemctl enable nfs-kernel-server

 

2.2.4 클라이언트 패키지 설치(모든 worker 노드에서만)

각 worker 노드에 NFS 관련 도구를 설치한다.

$ sudo apt update
$ sudo apt install -y nfs-common

 

2.2.5 연결 테스트(worker 노드 중 한곳에서)

worker 노드에서 master 노드의 NFS 공유 디렉토리가 정상적으로 접근되는지 확인한다.

$ showmount -e <master노드IP>
Export list for 192.168.0.36:
/srv/nfs/kubedata *

 

3. 영구 볼륨 생성

k8s 에서 영구 볼륨을 생성할 때는 아래와 같은 항목들을 설정할 수 있다.

  • 레이블: metadata.labels
  • 용량: spec.capacity.storage
  • 접근 모드: spec.accessModes
  • Reclaim Policy: spec.persistentVolumeReclaimPolicy
  • 마운트 옵션: spec.mountOptions
  • 스토리지 클래스: spec.storageClassName
  • 각 플러그인 고유 설정: spec.nfs 등

아래는 nfs 볼륨 플러그인을 사용하여 영구 볼륨을 구성하는 매니페스트이다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv-nfs
  labels: # 파드가 특정 PV를 찾을 수 있게 돕는 이름표
    type: nfs
    env: practice
spec:
  capacity:
    storage: 500Mi # 사용할 용량
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual # 수동으로 만든 PV임을 명시 (중요)
  mountOptions: # NFS 마운트 최적화 옵션
    - nfsvers=4.1 # NFS 버전 명시
    - hard # 서버 장애 시 응답이 올 때까지 대기 (데이터 보호)
    - timeo=600 # 타임아웃 설정
    - retrans=2 # 재전송 횟수
  nfs:
    path: /srv/nfs/kubedata # 마스터 노드에서 만든 경로
    server: 192.168.0.36 # 마스터 노드 IP 주소

PV 생성 후 아래 커맨드로 조회해본다. STATUS 항목이 Available 로 뜨면 사용 준비가 된 것이다.

$ kubectl get pv sample-pv-nfs
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
sample-pv-nfs   500Mi      RWO            Retain           Available           manual         <unset>                          41s

 

PV 에 설정하는 각 항목을 살펴보자

 

3.1 레이블

동적 프로비저닝(StorageClass 를 지정하여 PVC 리소스를 생성하는 타이밍에 PV 를 동적으로 생성하여 할당하는 기법)을 사용하지 않고, 미리 영구 볼륨을 생성해두고 나중에 사용하려는 경우 영구 볼륨을 식별하기 어렵기 때문에 type, env, environment, speed 등의 레이블을 사용하면 편리하다.

 

3.2 용량

여기서 지정하는 용량은 실제 디스크의 공간을 "예약" 하지도, "보장" 하지도 않는다. PVC <- PV 할당을 위한 스케쥴링/바인딩을 위한 논리값일 뿐이다. 실제 파일시스템의 용량과 무관하다. 파일을 실제 write 할 때 그만큼만 디스크 공간이 사용된다.

PV에 용량을 지정할 때 주의할 점은, PVC 에서 요청하는 용량 이상 중 가장 낮은 용량을 가진 PV 가 할당된다는 것이다. 만약 1Gi, 20Gi 의 PV 가 있고, PVC 에서 2Gi 를 요청하는 경우에 무려 20Gi 의 PV 가 할당된다는 점이다.

 

3.3 접근 모드

접근 모드는 아래 3가지가 있다.

단축어 설명
ReadWriteOnce RWO 단일 노드에서 Read/Write 가능
ReadWriteMany RWX 여러 노드에서 Read/Write 가능
ReadOnlyMany ROX 여러 노드에서 Read 가능

이 중에서 ReadOnlyMany 를 사용할때는 주의할 점이 있다. 여러 노드에서 해당 볼륨에 읽기 작업을 해야하는데, 그 중 하나라도 쓰기 요청하는 파드가 있다면 다른 노드에서 볼륨 마운트가 불가능해진다. 따라서 이 볼륨을 사용하는 모든 파드에서는 영구 볼륨 클레임을 지정할 때 readOnly: true 로 지정해야 한다. 헷갈릴 수 있는데, 컨테이너 템플릿의 볼륨 마운트 설정인 spec.containers[].volumeMounts[].readOnly 항목이 "아니라" 볼륨 설정인 spec.volumes[].persistentVolumeClaim.readOnly 를 true 로 지정해야 한다.

ReadWriteMany 는 nfs 같은 볼륨 플러그인에서 주로 사용한다. ReadWriteMany 를 지원하지 않는 볼륨 플러그인도 있음에 주의하자.

 

3.4 Reclaim Policy

PVC(영구 볼륨 클레임)에서 PV를 사용하고 난 후 PVC가 삭제되었을 때 PV의 처리 정책을 설정하는 것이다. 즉, 바인딩이 해제된 뒤의 처리 방식을 결정한다. spec.persistentVolumeReclaimPolicy 항목으로 설정하며, 아래 값이 올 수 있다.

설명 상태 변경 흐름
Delete 삭제한다. 동적 프로비저닝에서 사용하는 경우가 많다. 단, 동적 프로비저닝을 사용하지 않으면서 삭제 컨트롤러가 존재하지 않으면 자동 삭제되지 않는다.(Static NFS 같은 경우) Bound → PVC 삭제 → Released → PV 삭제 → 스토리지 데이터 삭제
Retain 유지한다. Released 상태가 된다. 하지만 또다른 PVC에 의해 자동으로 재사용(마운트)되지는 않는다. Bound → PVC 삭제 → Released → (수동 처리)
Recycle 유지한다. 단, 스토리지 데이터는 삭제(rm -rf ./*)한다. Available 상태가 되므로 또다른 PVC에 의해 자동으로 재사용될 수 있다. 하지만, deprecated 이므로 사용 지양. Bound → PVC 삭제 → 스토리지 데이터 삭제 → Available

Released 상태의 PV 는 PVC 에서 바인딩하지 않는다는 사실을 기억하자. Retain 의 경우에는 실제 스토리지 데이터도 삭제되지 않기 때문에, 새로운 PV 를 생성하여 데이터를 유지할 수 있다.

 

3.5. 마운트 옵션

마운트 옵션은 영구 볼륨 플러그인에 따라 다르기 때문에 각 사양을 확인해보자.

 

3.6 스토리지 클래스

PV의 스토리지 클래스 항목은 해당 PV가 어떤 스토리지 클래스를 통해서 바인딩되는지에 대한 항목이다. 동적 프로비저닝시에는 해당 스토리지 클래스의 CSI provisioner 에 의해 PV 가 자동 생성된다. 수동으로 PV를 관리하는 경우에는 해당 스토리지 클래스의 provisioner 는 kubernetes.io/no-provisioner 가 될 것이며, PVC 와 PV 모두에 스토리지 클래스 이름을 지정해주어야 한다.

아래는 스토리지 클래스 자체에 대해서 더 설명하겠다.

StorageClass 는 동적 프로비저닝을 위한 스토리지 정책을 담고 있는 리소스이다. CSI 드라이버를 통해서 PV 를 생성/바인딩 하므로 스토리지 정책을 입힐 수 있으며, 아래와 같은 항목들을 설정할 수 있다.

항목 설명
provisioner 프로비저너 CSI 드라이버를 지정한다. 
parameters 프로비저닝 파라미터를 지정한다. 디스크 타입, 복제 개수, 파일시스템 유형, NFS 서버 주소, 스토리지 풀 이름 등을 프로비저너 CSI 드라이버로 전달할 수 있다.
reclaimPolicy PVC 삭제시 PV 처리 정책을 지정한다.(Delete / Retain)
volumeBindingMode PV 바인딩 시점을 지정한다.(Immediate / WaitForFirstConsumer)
allowVolumeExpansion PVC 용량 확장 허용 여부를 지정한다.

PVC의 spec.storageClassName 에 사용할 스토리지 클래스 이름을 지정하면 PVC가 생성될 때 해당 스토리지 클래스를 사용하여 PV를 자동으로 생성하여 바인딩하게 된다.(동적 프로비저닝)

PVC 생성 → StorageClass 참조 → CSI provisioner 호출 → PV 자동생성 → PVC 와 바인딩

동적 프로비저닝을 사용하지 않고도 스토리지 클래스를 사용하려면, 스토리지 클래스의 provisioner 에 kubernetes.io/no-provisioner 를 지정하고, PV 와 PVC 의 spec.storageClassName 에 해당 스토리지 클래스 이름을 지정하면 된다.

아래는 동적 프로비저닝을 사용하지 않는 StorageClass 매니페스트 예시이다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: manual
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate

 

3.7 각 플러그인 고유 설정

각 영구 볼륨 플러그인의 사양을 확인해보자.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

볼륨이란, 호스트에 미리 준비된 사용 가능한 디렉토리를 일컫는다. 쿠버네티스에서는 볼륨 리소스를 생성/삭제할 수 없다. 파드에서 볼륨을 사용하기 위해 매니페스트에 볼륨을 지정하기만 할 수 있다. 볼륨은 플러그인 형태로 제공되는데, ConfigMap 과 Secret 도 볼륨 플러그인의 종류이다. 대표적인 볼륨 플러그인은 아래와 같다.

  • emptyDir
  • hostPath
  • downwardAPI
  • projected
  • nfs
  • iscsi
  • cephfs

1. emptyDir

노드에서 파드로 할당되는 호스트의 임시 디스크 영역으로, 파드가 종료되면 같이 삭제된다. 직접 마운트할 호스트의 영역을 지정할 수는 없다. 아래 매니페스트와 같이 emptyDir 볼륨을 마운트할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir: {}

아래 커맨드로 컨테이너의 /cache 마운트 경로에 할당된 디스크를 확인해보자

$ kubectl exec -it sample-emptydir -- df -h | grep cache
/dev/mapper/ubuntu--vg-ubuntu--lv   11G  6.6G  3.1G  69% /cache

emptyDir.sizeLimit 항목을 사용하여 해당 볼륨의 사이즈 제한을 걸 수 있다. 아래 매니페스트처럼 지정한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-limit
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir:
        sizeLimit: 128Mi

아래 커맨드와 같이 pod 를 --watch 옵션으로 감시하며, 컨테이너 내부에서 볼륨 마운트 된 경로에 150MB 용량의 더미 파일을 생성한다.

$ kubectl get pods --watch
NAME                                READY   STATUS    RESTARTS      AGE
sample-emptydir-limit               1/1     Running   0             2m57s

# 별도의 세션에서 실행
$ kubectl exec -it sample-emptydir-limit -- dd if=/dev/zero of=/cache/dummy bs=1M count=150
150+0 records in
150+0 records out
157286400 bytes (157 MB, 150 MiB) copied, 0.803752 s, 196 MB/s

# watch 중인 파드 상태가 아래처럼 바뀌었음을 확인
NAME                                READY   STATUS      RESTARTS      AGE
sample-emptydir-limit               0/1     Completed   0             5m45s

$ kubectl describe pods sample-emptydir-limit
...
Events:
  Type     Reason     Age    From               Message
  ----     ------     ----   ----               -------
  ...
  Warning  Evicted    5m46s  kubelet            Usage of EmptyDir volume "cache-volume" exceeds the limit "128Mi".
  Normal   Killing    5m46s  kubelet            Stopping container nginx-container

용량 초과로 인해 파드가 종료된 것을 알 수 있다.

 

emptyDir 은 호스트 노드의 디스크 뿐 아니라 RAM 영역인 tmpfs 영역에서 사용할 수도 있다. 아래처럼 emptyDir.medium 항목에 Memory 를 설정한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-memory
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir:
        medium: Memory
        sizeLimit: 128Mi

다시 아래 커맨드로 확인해보면 노드의 tmpfs 영역이 볼륨으로 마운트된 것을 볼 수 있다.

$ kubectl exec -it sample-emptydir-memory -- df -h | grep cache
tmpfs                              128M     0  128M   0% /cache

하지만, Memory 영역을 마운트한 경우에는 sizeLimit 을 초과한 용량을 사용하려고 하면 한도까지만 write 가 되고, 파드가 종료되지는 않는다는 차이가 있다.(공식문서에는 해당 스펙이 기재되어 있지 않지만 실제로 이렇게 동작함. kublet 설정에 따라 다를 수 있음.)

$ kubectl exec -it sample-emptydir-memory -- dd if=/dev/zero of=/cache/dummy bs=1M count=150

$ kubectl get pods
NAME                                READY   STATUS      RESTARTS      AGE
sample-emptydir-memory              1/1     Running     0             16m

# 128M 까지만 기록됨
$ kubectl exec -it sample-emptydir-memory -- ls -lh /cache/dummy
-rw-r--r-- 1 root root 128M Jan 27 14:54 /cache/dummy

파드에는 컨테이너별로 사용할 수 있는 메모리 제한을 걸 수 있는데, 이는 볼륨의 emptyDir.sizeLimit 에 지정한 메모리 제한보다 더 우선 적용되는 항목이기 때문에, 아래 매니페스트로 파드를 생성한 후 70MB의 더미 파일을 만들면 OOM에 의해 파드가 종료된다. 파드의 컨테이너 메모리 제한은 spec.containers[].resources.limits.memory 에 지정할 수 있다.(실제로 해보면 나의 경우에는 파드가 종료되지 않는다...)

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-memory-with-memory-limits
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      resources:
        limits:
          memory: 64Mi
      volumeMounts:
        - mountPath: /cache
          name: cache-volume
  volumes:
    - name: cache-volume
      emptyDir:
        medium: Memory
        sizeLimit: 128Mi

 

2. hostPath

hostPath 는 emptyDir 과 다르게 호스트 노드의 지정한 영역을 마운트하는 방식이다. 파드가 종료되어도 호스트의 해당 영역은 그대로 유지된다. type 을 지정해야 하는데, 지정 가능한 값은 아래와 같다.

type 설명
DirectoryOrCreate 지정한 경로에 디렉토리가 없으면 0755 권한으로 빈 디렉토리를 생성
Directory 지정한 경로에 디렉토리가 없으면 파드 생성에 실패
FileOrCreate 지정한 경로에 파일이 없으면 0644 권한으로 빈 파일을 생성
File 지정한 경로에 파일이 없으면 파드 생성에 실패
Socket 지정한 경로에 UNIX 소켓 파일이 필요(Docker/CRI 소켓 연결시 사용)
CharDevice 지정한 경로에 문자 디바이스가 필요
BlockDevice 지정한 경로 블록 디바이스가 필요

하지만, hostPath 는 파드가 노드의 파일 시스템에 직접 접근하므로 보안상 위험하여 권장되지 않는다.

매니페스트 예시는 아래와 같다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-hostpath
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: hostpath-sample
          mountPath: /srv
  volumes:
    - name: hostpath-sample
      hostPath:
        path: /etc
        type: DirectoryOrCreate
# 호스트 노드의 OS 정보 확인
$ kubectl exec -it sample-hostpath -- cat /srv/os-release | grep "PRETTY_NAME"
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

 

3. downwardAPI

downwardAPI 는 파드 정보나 컨테이너 정보를 파일로 컨테이너 내에 배치하기 위한 플러그인이다. 파드의 정보는 fieldRef 로, 컨테이너의 정보는 resourceFieldRef 로 가져올 수 있다. 아래 매니페스트를 적용해보자.

apiVersion: v1
kind: Pod
metadata:
  name: sample-downward-api
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: downward-api-volume
          mountPath: /srv
  volumes:
    - name: downward-api-volume
      downwardAPI:
        items:
          - path: "podname"
            fieldRef:
              fieldPath: metadata.name
          - path: "cpu-request"
            resourceFieldRef:
              containerName: nginx-container
              resource: requests.cpu

아래 커맨드로 파드명과 컨테이너의 CPU 요구사항이 파일로 배치되었는지 확인한다.

$ kubectl exec -it sample-downward-api -- tail -n +1 /srv/podname /srv/cpu-request
==> /srv/podname <==
sample-downward-api
==> /srv/cpu-request <==
0

 

4. projected

projected 는 Secret, ConfigMap, downwardAPI, serviceAccountToken 를 하나의 볼륨 마운트 경로에 통합하는 플러그인이다. 여러가지 볼륨으로 관리되는 기밀/설정 정보를 컨테이너 내 하나의 디렉터리에 배치하고자 할 때 사용한다. 아래의 매니페스트 예시를 보자.

(sample-db-auth 시크릿과 sample-configmap 컨피그맵은 미리 생성되어 있어야 한다. 없다면 아래를 참고하여 생성하자.)

apiVersion: v1
kind: Pod
metadata:
  name: sample-projected
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: projected-volume
          mountPath: /srv
  volumes:
    - name: projected-volume
      projected:
        sources:
          - secret:
              name: sample-db-auth
              items:
                - key: username
                  path: secret/username.txt
          - configMap:
              name: sample-configmap
              items:
                - key: nginx.conf
                  path: configmap/nginx.conf
          - downwardAPI:
              items:
                - path: "podname"
                  fieldRef:
                    fieldPath: metadata.name
$ kubectl exec -it pod/sample-projected -- tail -n +1 /srv/secret/username.txt /srv/configmap/nginx.conf /srv/podname
==> /srv/secret/username.txt <==
root
==> /srv/configmap/nginx.conf <==
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

==> /srv/podname <==
sample-projected

sample-db-auth 시크릿의 username 값은 base64 디코딩되어 마운트되는 것을 알 수 있다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

컨피그맵이나 시크릿의 데이터를 파드에서 사용할 때 볼륨 마운트 방식으로 사용할 수 있다.

이때 마운트된 볼륨 내의 파일들에 대해서 permission 을 설정해 줄 수 있다. 컨피그맵과 시크릿 모두 기본 퍼미션은 0644(rw-r--r--)이다. 퍼미션은 매니페스트 파일로 작성할 때 8진수 퍼미션 값을 10진수로 변환하여 기입하여야 한다.

8진수(Octal Mode) 10진수 rwx 표기(Symbolic Mode)
0400 256 r--------
0600 384 rw-------
0644 420 rw-r--r--
0700 448 rwx------
0755 493 rwxr-xr-x
0777 511 rwxrwxrwx

 

1. ConfiMap 예시

아래의 매니페스트는 컨피그맵의 쉘 스크립트 파일을 0755 권한으로 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-scripts
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      command: ["/config/test.sh"]
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
        items:
          - key: test.sh
            path: test.sh
            mode: 493 # 0755

아래 커맨드로 쉘 스크립트 실행을 확인해본다.

$ kubectl apply -f sample-configmap-scripts.yaml

# 권한 확인
$ kubectl exec -it sample-configmap-scripts -- ls -l /config/test.sh
lrwxrwxrwx 1 root root 14 Jan 17 08:01 /config/test.sh -> ..data/test.sh

# 실행됨 확인
$ kubectl logs sample-configmap-scripts
Hello, kubernetes

 

그런데, 권한 확인을 해보니 0755 권한은 rwxr-xr-x 이어야 하는데 rwxrwxrwx 로 출력된다. 쿠버네티스에서 Secret 이나 ConfigMap 을 볼륨으로 마운트하면 Atomic Reference Updates 라는 메커니즘에 의해 파일들이 복잡한 심볼릭 링크 구조를 가지기 때문이다. 심볼릭 링크가 걸린 경로인 "..data/test.sh" 는 실제 데이터 경로가 아니라 쿠버네티스가 데이터 업데이트시 원자성을 보장하기 위해 만든 중간 경로이다. 예를 들어 하나의 시크릿에 10개의 key-value 가 존재하고, 각각의 key-value 를 파일로 만들어서 파드에 볼륨 마운트 한다고 치자. 어느순간 시크릿이 수정되어 10개의 마운트된 파일들이 모두 수정되어야 하는데, 그 중 5개만 수정된 상태에서 애플리케이션에서 마운트된 경로에 접근했을 경우 5개의 파일은 이전버전의 시크릿을, 5개의 파일은 현재버전의 시크릿을 읽게 되어 일관성이 무너지게 된다. 따라서 10개의 파일을 모두 업데이트 한 후, 어느 시점에 한번에 마운트 링크를 변경시키는 방식을 사용하는 것이다. 실제로 마운트 경로의 파일을 모두 출력해보면 "..data" 라는 심볼릭 링크가 있고, 다시 타임스탬프 형식으로 이루어진 디렉토리를 참조하는 형태임을 알 수 있다. 복잡한 심볼릭 링크를 따라가서 실제 파일의 경로를 보려면 -L 옵션(dereference)을 사용한다.

# 심볼릭 링크가 가리키는 중간 심볼릭 링크와 실제 데이터가 저장된 디렉토리를 볼 수 있다.
$ kubectl exec -it sample-configmap-scripts -- ls -alF /config
total 12
drwxrwxrwx 3 root root 4096 Jan 17 08:01 ./
drwxr-xr-x 1 root root 4096 Jan 17 08:01 ../
drwxr-xr-x 2 root root 4096 Jan 17 08:01 ..2026_01_17_08_01_01.3453801296/
lrwxrwxrwx 1 root root   32 Jan 17 08:01 ..data -> ..2026_01_17_08_01_01.3453801296/
lrwxrwxrwx 1 root root   14 Jan 17 08:01 test.sh -> ..data/test.sh*

# -L 옵션으로 심볼릭 링크를 쭉 따라가서 원본 파일 정보를 출력할 수 있다.
$ kubectl exec -it sample-configmap-scripts -- ls -lL /config/test.sh
-rwxr-xr-x 1 root root 52 Jan 17 08:01 /config/test.sh

 

2. Secret 예시

아래의 매니페스트는 시크릿의 모든 key-value 를 0400 권한으로 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-secure
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      secret:
        secretName: sample-db-auth
        defaultMode: 256

아래 커맨드로 권한을 확인해본다.

$ kubectl exec -it sample-secret-secure -- ls -l /config/..data/
total 8
-r-------- 1 root root 12 Jan 25 07:39 password
-r-------- 1 root root  4 Jan 25 07:39 username

 

3. 동적 업데이트

위의 컨피그맵 예시에서 사용한 sample-configmap 의 모든 키를 파드에 볼륨 마운트 한 후, 컨피그맵의 thread 값만 "16" -> "32" 로 변경한 후 파드 재시작 없이도 변경된 값이 마운트 경로에서도 변경되는지 확인해보자.

먼저 아래 매니페스트를 적용하여 sample-configmap 컨피그맵의 모든 key-value 를 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
$ kubectl exec -it sample-configmap-multi-volume -- cat /config/thread
16

이 상태에서 sample-configmap 매니페스트의 thread 값을 32로 변경 후 apply 한다.

기본 업데이트 간격은 60초로 설정되어 있기 때문에 바로 변경되지 않더라도 최대 60초 후에는 변경된 것을 확인할 수 있다.

$ kubectl exec -it sample-configmap-multi-volume -- cat /config/thread
32

 

4. 변경 불가능한 컨피그맵/시크릿 만들기

아래처럼 매니페스트에 immutable: true 항목을 추가하면 컨피그맵이나 시크릿을 한번 생성하면 도중에 절대 변경 불가능하도록 만들 수 있다.

apiVersion: v1
kind: Secret
metadata:
  name: my-secure-config
type: Opaque
immutable: true  # <--- 이 항목을 추가합니다.
data:
  api-key: AAA

kubectl apply, patch, edit 같은 명령어는 물론 어떤 방법으로도 data 를 변경할 수 없다. 데이터를 변경하려면 리소스를 삭제 후 변경된 data 를 포함하여 재생성하여야만 한다. 또한, immutable 항목 자체도 한번 생성하면 변경 불가능하다. 아래와 같은 이점이 있다.

  • Kubelet 부하 감소: 쿠버네티스의 워커 노드(Kubelet)는 Secret이 변경되었는지 주기적으로 API Server를 호출하여 감시(Watch)한다. immutable: true로 설정된 리소스는 변경되지 않음을 보장받으므로, Kubelet이 감시를 중단하여 클러스터 전체의 부하를 줄일 수 있다.
블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

컨피그맵은 파드의 컨테이너에서 참조하여 사용하게 된다. 환경 변수로 전달하는 방법과 볼륨으로 마운트하여 사용하는 방법이 있다.

  • 환경 변수로 전달
    • 컨피그맵의 특정 키 전달: spec.containers[].env[].valueForm.configMapKeyRef.name/key
    • 컨피그맵 전체 키 전달: spec.containers[].envFrom[].configMapRef.name
  • 볼륨으로 마운트
    • 컨피그맵의 특정 키 마운트: spec.volumes[].configMap.items[].key/path
    • 컨피그맵 전체 키 마운트: spec.volumes[].configMap.name

1. 환경 변수로 특정 키 전달

아래 매니페스트와 같이 env[] 에 환경변수들을 이름과 값으로 하나씩 직접 지정한다. sample-configmap 이라는 컨피그맵에 connection.max 키로 담긴 값을 CONNECTION_MAX 라는 환경변수로 세팅하는 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-single-env
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      env:
        - name: CONNECTION_MAX
          valueFrom:
            configMapKeyRef:
              name: sample-configmap
              key: connection.max
$ kubectl apply -f sample-configmap-single-env.yaml
$ kubectl exec -it sample-configmap-single-env -- env | grep CONNECTION_MAX
CONNECTION_MAX=100

 

2. 환경 변수로 전체 키 전달

직접 환경변수명과 불러올 컨피그맵 값을 지정하지 않고 컨피그맵 전체를 환경변수로 그대로 등록하는 방법이다. 간단하긴 하지만, 매니페스트만 봐서는 어떤 환경변수가 설정되는지 알 수 없다는 단점이 있다.

아래 매니페스트처럼 간단히 envFrom[] 에 가져올 컨피그맵들의 참조를 걸어주면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-env
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      envFrom:
        - configMapRef:
            name: sample-configmap
$ kubectl apply -f sample-configmap-multi-env.yaml
$ kubectl exec -it sample-configmap-multi-env -- env
...
connection.max=100
connection.min=10
nginx.conf=user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

sample.properties=property.1=value-1
property.2=value-2
property.3=value-3

test.sh=#!/bin/bash
echo "Hello, kubernetes"
sleep infinity

thread=16
...

그러나 위처럼 파드에 설정된 환경변수를 출력해보면 nginx.conf, sample.properties, test.sh 가 제대로 설정되지 못했음을 알 수 있다.(개행 처리가 의도대로 되지 않음) 개행된 값을 컨테이너로 주입하고자 할 때는 볼륨 마운트 방식으로 사용하여야 한다. 아래와 같은 주의사항을 참고한다.

  • 환경 변수명에 "." 또는 "-" 가 포함되지 않도록 한다.
  • 환경 변수 값이 개행을 포함하지 않도록 한다.(yaml 문법의 <key>: | 개행 스타일 포함)

 

3. 볼륨으로 특정 키 마운트

컨피그맵의 정보를 컨테이너의 볼륨으로 마운트 하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 값을 가져올 컨피그맵 및 읽을 key, 그리고 마운트 경로에 생성할 파일명(path)을 지정한다. 직접 마운트 파일명을 지정한다는 것이 특징이다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-single-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
        items:
          - key: nginx.conf
            path: nginx-sample.conf
$ kubectl apply -f sample-configmap-single-volume.yaml
$ kubectl exec -it sample-configmap-single-volume -- cat /config/nginx-sample.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

 

4. 볼륨으로 전체 키 마운트

컨피그맵의 전체 정보를 컨테이너의 볼륨으로 마운트하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 가져올 컨피그맵을 지정한다. 마운트 경로에 생성되는 파일명은 컨피그맵의 key 와 동일하다. 매니페스트는 간단해지지만 매니페스트만으로는 컨피그맵의 어떤 key-value가 마운트되는지 알 수 없다는 단점이 있다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
$ kubectl apply -f sample-configmap-multi-volume.yaml
$ kubectl exec -it sample-configmap-multi-volume -- ls /config
connection.max	connection.min	nginx.conf  sample.properties  test.sh	thread
$ kubectl exec -it sample-configmap-multi-volume -- paste /config/connection.max /config/nginx.conf /config/sample.properties
100	user nginx;	property.1=value-1
	worker_processes auto;	property.2=value-2
	error_log /var/log/nginx/error.log;	property.3=value-3
	pid /run/nginx.pid;

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

ConfigMap 은 key-value 들을 저장하는 리소스이다. value 에는 바이너리 데이터나 nginx.conf 같은 설정 내용도 저장할 수 있다. 하나의 ConfigMap에는 1MB 까지 저장 가능하다. 생성하는 방법은 Generic 타입의 Secret 과 거의 동일하며, 3가지 생성하는 방법을 설명한다.

 

1. 매니페스트에서 생성

일반적인 리소스 생성 방식과 동일하게 kubectl apply -f <매니페스트파일명> 또는 kubectl create -f <매니페스트파일명> --save-config 으로 생성하는 방식이다. 시크릿과는 달리 base64 인코딩되지 않는다. value 를 여러 행으로 저장하는 경우(nginx 설정파일 내용 등) YAML 문법대로 <key>: | 로 시작하여 여러 행에 정의한다. 숫자는 큰따옴표로 둘러쌓아야 한다. 아래 예시를 참고한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-configmap
data:
  thread: "16"
  connection.max: "100"
  connection.min: "10"
  sample.properties: |
    property.1=value-1
    property.2=value-2
    property.3=value-3
  nginx.conf: |
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log;
    pid /run/nginx.pid;
  test.sh: |
    #!/bin/bash
    echo "Hello, kubernetes"
    sleep infinity

 

아래 커맨드로 저장된 key-value 를 확인해본다.

$ kubectl get configmaps sample-configmap -o yaml
apiVersion: v1
data:
  connection.max: "100"
  connection.min: "10"
  nginx.conf: |
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log;
    pid /run/nginx.pid;
  sample.properties: |
    property.1=value-1
    property.2=value-2
    property.3=value-3
  test.sh: |
    #!/bin/bash
    echo "Hello, kubernetes"
    sleep infinity
  thread: "16"
kind: ConfigMap
...

 

2. 리터럴 값을 전달하여 생성

kubectl 커맨드로 시크릿을 생성하되, --from-literal 옵션으로 직접 key-value 쌍을 전달하는 방식이다. 아래처럼 생성한다.

# 생성
$ kubectl create configmap --save-config web-config \
  --from-literal=connection.max=100 --from-literal=connection.min=10

# 확인
$ kubectl get cm web-config -o json | jq '.data'
{
  "connection.max": "100",
  "connection.min": "10"
}

 

3. 파일의 내용을 value 로 생성

kubectl 커맨드로 생성하되, --from-file 옵션으로 key-value 로 사용될 파일들을 지정하는 방식이다. 파일명이 key 가 되고, 파일 내용이 value 가 된다. 만약 파일명 대신 다른 key 를 사용하고자 한다면 --from-file=<사용할key>=<파일명> 형식으로 입력한다.

# 생성
$ kubectl create configmap --save-config sample-configmap-file \
> --from-file=./nginx.conf

# 확인
$ kubectl get configmaps sample-configmap-file -o json | jq '.data'
{
  "nginx.conf": "user  nginx;\nworker_processes  auto;\nerror_log  /var/log/nginx/error.log warn;\npid        /var/run/nginx.pid;\n"
}

# describe 확인
$ kubectl describe configmaps sample-configmap-file
Name:         sample-configmap-file
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
nginx.conf:
----
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;



BinaryData
====

Events:  <none>

 

k8s 에 실제 등록하지 않고 매니페스트 파일로 출력하려면 --dry-run=client -o yaml 옵션을 주어 실행한다. UTF-8 이외의 데이터를 저장하면 자동으로 binaryData 필드로 base64 인코딩 되어 저장된다.

$ kubectl create configmap sample-configmap-binary-2 \
  --from-file=image.png \
  --from-literal=index.html="Hello, Kubernetes" \
  --dry-run=client -o yaml \
  > sample-configmap-binary-2.yaml
# sample-configmap-binary-2.yaml 매니페스트
apiVersion: v1
binaryData:
  image.png: iVBORw0KGgoAAAANSUhEUgAAAs...<생략>
data:
  index.html: Hello, Kubernetes
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: sample-configmap-binary-2

이제 이를 기반으로 ConfigMap 을 생성한 후, 아래 매니페스트로 ConfigMap 을 볼륨 마운트 한 파드를 생성한 후, curl 로 호출해보자

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-binary-webserver
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /usr/share/nginx/html
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap-binary-2
# ConfigMap 생성
$ kubectl apply -f sample-configmap-binary-2.yaml

# Pod 생성
$ kubectl apply -f sample-configmap-binary-webserver.yaml

# 확인을 위한 포트 포워딩
$ kubectl port-forward sample-configmap-binary-webserver 8080:80

# 호출 확인
$ curl http://localhost:8080/index.html
Hello, Kubernetes

# 볼륨 마운트 경로에 파일 생성 확인
$ kubectl exec -it sample-configmap-binary-webserver -- ls /usr/share/nginx/html
image.png  index.html

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,