この記事は Kubernetes道場 Advent Calendar 2018 12日目の記事です。

今回はPersistentVolumeとPersistentVolumeClaimとStorageClassについて。

永続化ボリュームについて

Kubernetesの永続化ボリュームについて、まず各オブジェクトについてどのようなものかを解説しよう。

永続化ボリュームに関するオブジェクト

StorageClass

StorageClassはストレージの種類を示すオブジェクトだ。例えば、通常・高可用性・高スループットなどだ。

また、プロビジョニングという永続化ボリュームを動的に作成する設定もこのStorageClassに行う。

PersistentVolume

字の通り、PersistentVolume(PV)は永続化ボリュームそれ自体についてのオブジェクトだ。

StorageClassを元に動的に作成されたものや、Kubernetes管理者によって追加されたボリュームも含まれる。

永続化ボリュームの種類は多くあり、それらについては以下のリンクを参考にしてほしい。

Types of Persistent Volumes - Persistent Volumes - Kubernetes

今回はローカル環境のminikubeを使用していることもあり、シングルノードのテスト向けである HostPath を主に見ていく。

PersistentVolumeClaim

PersistentVolumeClaim(PVC)は永続化ボリュームの利用請求をするオブジェクトだ。

永続化ボリュームを使用する際は、このPersistentVolumeClaimリソースを作成する。

その後、クレームに従ったものが自動的にプロビジョニングされるか、Kubernetesの管理者がクレームに従ったものを作成するかをして永続化ボリュームが払い出される。

全体の関係図

全体の大まかな関係図は以下のようになる。

各設定項目について

各オブジェクトのざっくりとした関係性は分かったと思うので、次は各オブジェクトの設定項目を見ていこう。

StorageClass

provisioner

Provisionerを指定する。

  • kubernetes.io/gce-pd
  • kubernetes.io/aws-ebs
  • kubernetes.io/glusterfs

など。詳しくは以下のドキュメントへ。

Provisioner - Storage Classes - Kubernetes

このProvisionerは自前のものを実装して組み込むことが可能だ。

それはまた別の機会に。

parameters

ProvisionerのパラメータをKey/Valueの形式で指定する。

詳しくは以下のドキュメントへ。

Provisioner - Storage Classes - Kubernetes

reclaimPolicy

PVCが削除された際にPVを削除( Delete )するか残すか( Retain )を指定する。

デフォルトは Delete だ。

volumeBindingMode

PVCが作成された際に、即時にPVを紐付けるか( Immediate )、実際使用される際に紐付けるか( WaitForFirstConsumer )を指定する。

WaitForFirstConsumer だとPVCが作成された際にはまだPVが作成されたりBindされたりしない。PVCが実際にPodで利用される際に作成・Bindingが行われるようになる。

デフォルトは Immediate だ。

mountOptions

StorageClassによって動的に作成される際のマウントオプションを指定する。

このオプションはチェックされず、無効なオプションの場合単に処理が失敗する。

allowVolumeExpansion

ボリュームの拡張を許可するかを指定する。

デフォルトはFalseだ。

allowedTopologies

動的にプロビジョニングする際の対象Nodeを制限することが出来る。 matchLabelExpressions でNodeのトポロジーを指定をする。

PersistentVolume

PVは殆どが各Volumeの設定についてのフィールドだ。共通で使用するフィールドのみ解説する。

accessModes

PVのアクセスモードを指定する。アクセスモードは現在以下の3種類。

  • ReadWriteOnce : 単一Nodeで読み書きが可能
  • ReadOnlyMany : 複数Nodeで読み込みが可能
  • ReadWriteMany : 複数Nodeから読み書きが可能

使用するボリュームによってサポートが変わるので以下のリンクから要確認だ。

Access Modes - Persistent Volumes - Kubernetes

capacity

PVの容量を指定する。指定方法はKeyに storage 、ValueはResourceRequirementでMemoryの容量を指定するのに近い。以下が例だ。

1
2
capacity:
  storage: 5Gi
mountOptions

StorageClassのフィールドと同様。

persistentVolumeReclaimPolicy

StorageClassの reclaimPolicy と同様。

storageClassName

PVのStorageClassを指定する。空の場合どのStorageClassにも属さないPVとして扱われる。

volumeMode

PVをファイルシステム( Filesystem )としてマウントさせるか、ブロックデバイス( Block )としてマウントさせるかを指定する。

デフォルトは Filesystem だ。

PersistentVolumeClaim

resources

最低限必要なボリュームの容量を指定する。指定方法はResourceRequirementsとほど同様だ。以下が指定方法の例だ。

1
2
3
resources:
  requests:
    storage: 5Gi

ここで指定した容量より大きいPVがBindingされることもある。

selector

PVのBindingでLabel Selectorを利用できる。 Deploymentなどでも使用した matchLabelsmatchLabelExpressions か指定できる。

PVと同様のフィールド
  • accessModes
  • storageClassName
  • volumeMode

Persistent Volumeを使ってみる

さて、APIドキュメントの翻訳みたいになってしまったが、ここからは例を出してどのような動きをするか見てみる。

PV / PVCを使ってPodにマウントする

まず、PVを作成しよう。PVCを作ってもPV自体が無いと何も始まらない。

以下のようなPVを作成した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0001
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: slow
  hostPath:
    path: /data/pv0001
    type: DirectoryOrCreate

今回はPVにhostPathを使用している。使用方法はPodでVolumeを使用した際と同様だ。

minikubeでは /data がVolumeとして使用できるようになっているので、そこにフォルダを一つ切ってPVとして使用することにする。

さて、上記のManifestを適用してみる。

1
2
$ kubectl apply -f pv0001.yaml
persistentvolume/pv0001 created

作成できたようだ。確認してみよう。

1
2
3
kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv0001   1Gi        RWO            Delete           Available           slow                    90s

指定通りに作成できているようだ。また、STATUSからPVが有効な状態だということも分かる。

今回作成したPVのStorageClassを slow とした。(名前に特に意味はない)

さて、このStorageClassを請求するPVCを作成しよう。今作成したPVがBindingされるはずだ。

以下のようなManifestを作成した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pv-slow-claim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi
  storageClassName: slow

StorageClassに slow を指定し、容量のリクエストも 1Gi にしている。

さて、このManifestを適用してPVとPVCを確認してみよう。

1
2
3
4
5
6
7
8
$ kubectl apply -f pvc.yaml
persistentvolumeclaim/pv-slow-claim created
$ kubectl get pv,pvc
NAME                      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   REASON   AGE
persistentvolume/pv0001   1Gi        RWO            Delete           Bound    default/pv-slow-claim   slow                    6m24s

NAME                                  STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pv-slow-claim   Bound    pv0001   1Gi        RWO            slow           42s

PVCを作成したら先程作成したPVがBindingされたのがSTATUSから分かるだろう。

それではこのPVCを元にPodを作成し、マウントしてみよう。

以下のようなManifestを作成した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
  name: pvc-slow-test
spec:
  containers:
  - image: alpine
    name: alpine
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: claim-volume
      mountPath: /data
  volumes:
  - name: claim-volume
    persistentVolumeClaim:
      claimName: pv-slow-claim
  terminationGracePeriodSeconds: 0

Podにマウントする際は、 spec.volumes[].persistentVolumeClaim.claimName に作成したPVCの名前を指定する。あとは今までどおりのボリュームマウントと同様だ。

さて、上記のManifestを適用して確認してみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ kubectl apply -f pvc-slow-test.yaml
pod/pvc-slow-test created
$ kubectl exec pvc-slow-test -- ls -l /
total 56
drwxr-xr-x    2 root     root          4096 Sep 11 20:23 bin
drwxr-xr-x    2 root     root          4096 Dec 12 08:43 data
drwxr-xr-x    5 root     root           360 Dec 12 08:43 dev
drwxr-xr-x    1 root     root          4096 Dec 12 08:43 etc
drwxr-xr-x    2 root     root          4096 Sep 11 20:23 home
drwxr-xr-x    5 root     root          4096 Sep 11 20:23 lib
drwxr-xr-x    5 root     root          4096 Sep 11 20:23 media
drwxr-xr-x    2 root     root          4096 Sep 11 20:23 mnt
dr-xr-xr-x  136 root     root             0 Dec 12 08:43 proc
drwx------    2 root     root          4096 Sep 11 20:23 root
drwxr-xr-x    1 root     root          4096 Dec 12 08:43 run
drwxr-xr-x    2 root     root          4096 Sep 11 20:23 sbin
drwxr-xr-x    2 root     root          4096 Sep 11 20:23 srv
dr-xr-xr-x   12 root     root             0 Dec 12 08:43 sys
drwxrwxrwt    2 root     root          4096 Sep 11 20:23 tmp
drwxr-xr-x    7 root     root          4096 Sep 11 20:23 usr
drwxr-xr-x   11 root     root          4096 Sep 11 20:23 var
$ kubectl exec pvc-slow-test -- ls -l /data
total 0

/data にマウント出来ていることが分かる。が、ちょっと達成感が・・・ 😅

PVC / StorageClassをPVを動的にProvisioningする

先程もStorageClassを指定はしたが、StorageClassの名前を指定しただった。

今回は動的なProvisioningを試してみよう。

実はデフォルトでStorageClassが定義されている。確認してみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ kubectl get sc
NAME                 PROVISIONER                AGE
standard (default)   k8s.io/minikube-hostpath   9h
$ kubectl get sc/standard -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.beta.kubernetes.io/is-default-class":"true"},"labels":{"addonmanager.kubernetes.io/mode":"Reconcile"},"name":"standard","namespace":""},"provisioner":"k8s.io/minikube-hostpath"}
    storageclass.beta.kubernetes.io/is-default-class: "true"
  creationTimestamp: "2018-12-11T23:22:57Z"
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
  name: standard
  resourceVersion: "421"
  selfLink: /apis/storage.k8s.io/v1/storageclasses/standard
  uid: b15dec7f-fd9b-11e8-81da-a89e63bcdf60
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Delete
volumeBindingMode: Immediate 

Standardという名前のStorageClassが定義されていることが分かる。

また、provisionerに k8s.io/minikube-hostpath が指定されている。このStorageClassを使用することで動的にPVが作成されそうだ。 (ここの処理は追うと素晴らしく長くなってしまうのでまたの機会に。)

それではこのStorageClassを使ったPVCを作成しよう。

以下のようなManifestを作成した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pv-standard-claim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard

重要なのは storageClassName に先程あったStorageClassの standard を指定することだ。

と、言いたいが、実はデフォルトでこのStorageClassが使用されるように設定されている。なので storageClassName を省略するとStorageClassが standard で作成される。

また省略した場合であって、空文字を指定した場合は別の解釈がされるので要注意。

それでは適用して確認してみよう。

1
2
3
4
5
6
7
8
$ kubectl apply -f pv-standard-claim.yaml
persistentvolumeclaim/pv-standard-claim created
$ kubectl get pv,pvc
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                       STORAGECLASS   REASON   AGE
persistentvolume/pvc-348227f2-fdec-11e8-81da-a89e63bcdf60   1Gi        RWO            Delete           Bound    default/pv-standard-claim   standard                11s

NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pv-standard-claim   Bound    pvc-348227f2-fdec-11e8-81da-a89e63bcdf60   1Gi        RWO            standard       12s

この様にPVCを作成しただけでPVが払い出され、Bindingされている状態になっているのが分かる。便利。

volumeBindingModeについて

ここまでできていれば十分な気もするが、せっかくなので volumeBindingMode についても少し見ておこう。

先程PVとPVCを作成した時やStorageClassを standard で作成したPVCは作成されてすぐにBindingされていた。

これだと先程もそうだったが、1度も実際にPodにマウントをして利用をしていないのにPVのリソースを取ってしまっている状態だ。

これを実際に使うときまで(Schedulingされるまで)遅延させてくれるのがこの volumeBindingMode だ。

デフォルトでは今までのように作成されてすぐにBindingする Immediate なのだが、実際にSchedulingされるまで遅延させるのが WaitForFirstConsumer だ。

挙動を確認してみよう。

実はこの WaitForFirstConsumer と動的なProvisioningが同時に出来るボリュームタイプは現状以下の3つだけだ。

  • AWSElacticBlockStore( kubernetes.io/aws-ebs )
  • GCEPersistentDisk( kubernetes.io/gce-pd )
  • AzureDisk( kubernetes.io/azure-disk )

今回ローカル環境で使用しているminikubeではこのボリュームタイプは対応していないため、 provisioner には kubernetes.io/no-provisioner を使用する。

さて、上記の内容に従って以下のようなStorageClassのManifestを作成した。

1
2
3
4
5
6
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: delay-bind
provisioner: kubernetes.io/no-privisioner
volumeBindingMode: WaitForFirstConsumer

また、このStorageClassに従ったPVも作成する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-delay-bind
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 1Gi
  hostPath:
    path: /data/pv-delay-bind
    type: DirectoryOrCreate
  persistentVolumeReclaimPolicy: Delete
  storageClassName: delay-bind

これらを適用してみる。

1
2
3
$ kubectl apply -f delay-bind.yaml -f pv-delay-bind.yaml
storageclass.storage.k8s.io/delay-bind created
persistentvolume/pv-delay-bind created 

それではこのStorageClassを請求するPVCを作成しよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pv-delay-bind-claim
spec:
  accessModes:
  - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi
  storageClassName: delay-bind

さて、このManifestを適用してPVとPVCを確認してみよう。

1
2
3
4
5
6
7
8
$ kubectl apply -f pv-delay-bind-claim.yaml
persistentvolumeclaim/pv-delay-bind-claim created
$ kubectl get pv,pvc
NAME                             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
persistentvolume/pv-delay-bind   1Gi        RWO            Delete           Available           delay-bind              61m

NAME                                        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pv-delay-bind-claim   Pending                                      delay-bind     5s

リソースを取得するとPVのSTATUSはAvailableでまだBindingされていない状態だ。また、PVCのほうはSTATUSがPendingになっている。

このように volumeBindingModeWaitForFirstConsumer にするとPVCを作成しただけではBindingにはならない。

それではPodのボリュームとして使ってみてどうなるか見てみよう。以下のManifestを作成する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
  name: pv-delay-bind-test
spec:
  containers:
  - image: alpine
    name: alpine
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: claim-volume
      mountPath: /data
  volumes:
  - name: claim-volume
    persistentVolumeClaim:
      claimName: pv-delay-bind-claim
  terminationGracePeriodSeconds: 0

さて、このManifestを適用してPVとPVCの状態を確認してみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ kubectl apply -f pv-delay-bind-test.yaml
pod/pv-delay-bind-test created
$ kubectl get po,pv,pvc
NAME                     READY   STATUS    RESTARTS   AGE
pod/pv-delay-bind-test   1/1     Running   0          43s

NAME                             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   REASON   AGE
persistentvolume/pv-delay-bind   1Gi        RWO            Delete           Bound    default/pv-delay-bind-claim   delay-bind              67m

NAME                                        STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pv-delay-bind-claim   Bound    pv-delay-bind   1Gi        RWO            delay-bind     6m1s

PodでPVCが利用されるようになり、PVがBindingされた。

今作成したPodが最初のConsumerとして扱われ、PVCとPVがBindingされた。

この機能は最初にも書いたが、実際に利用するまでPVのBindingや動的なProvisioningであればPVの確保も遅らせることができ、リソースの節約になる。


というわけで今回はここまで。

次回はStatefulSet / DaemonSetについて見ていこう。

それでは。