'2026/02/16'에 해당되는 글 2건

영구 볼륨을 미리 생성해두고, 영구 볼륨 클레임 요청에 따라 바인딩하여 사용하는 기본 방식에는 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>

 

블로그 이미지

망원동똑똑이

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

,