k8s永続化ボリューム StorageClass, PersistentVolume, PersistentVolumeClaimまとめ

Kubernetesなどのコンテナ系を利用していると、Pod内にデータをためておくことができないため、ファイルを保存しておく外部のストレージが必要になってくる。昔のようにローカルの/var/dataとかに適当に保存すればええべって話にはなりまてん。
そんな時に使われるのが、k8sのStorageClass, PersistentVolume(PV), PersistentVolumeClaim(PVC)。

ログなどは適当な既存のソリューションを使えばいいし、設定ファイルレベルのものであればconfigmapというものがあるのでそちらを使えばいい。
今回は、geoipのDataMind？のmmdbファイルを複数のpodで共有する必要がでてきたためまとめてみた。mmdbファイルは定期的に更新されているので、とってきて更新する必要がある。

# TL;DR

1台のworker nodeでLocalのディレクトリを複数のpodから参照する流れでまとめています。


StorageClassでどんなストレージ(ボリューム)を使うかを定義し、PersistentVolume(PV)で永続化ボリュームとして定義する、PersistentVolumeClaim(PVC)で利用要求をする感じ。

環境

```
murata:~ $ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-11T18:14:22Z", GoVersion:"go1.13.6", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
```

# StorageClass

下記のような感じで定義ファイルを作成する。
ローカルディレクトリだったり、AWSのEBSやNFSサーバ、GCPのGCEPersistentDiskなどを利用できる。
公式: https://kubernetes.io/docs/concepts/storage/storage-classes/

```yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: geoip-db-local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate
reclaimPolicy: Retain
```

volumeBindingModeは実際にPodでPVCを使うときにバインドするか、即時にバインドしてしまうかの違い。

describeなどはこんな感じ。

```bash

murata:/var/lib $ kubectl get sc -o wide
NAME                     PROVISIONER                    AGE
geoip-db-local-storage   kubernetes.io/no-provisioner   32s
```

```bash

murata:/var/lib $ kubectl describe storageclass.storage.k8s.io/geoip-db-local-storage
Name:            geoip-db-local-storage
IsDefaultClass:  No
Annotations:     kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"geoip-db-local-storage"},"provisioner":"kubernetes.io/no-provisioner","reclaimPolicy":"Retain","volumeBindingMode":"Immediate"}

Provisioner:           kubernetes.io/no-provisioner
Parameters:            <none>
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Retain
VolumeBindingMode:     Immediate
Events:                <none>
```


# PersistentVolume(PV)

永続化ボリューム、↑のStrageClassで定義されたものを利用したり、ほかにもいろいろあるらしい。
読み書きのアクセスを設定したり、マウントオプションなどを設定できる。

公式: https://kubernetes.io/docs/concepts/storage/persistent-volumes/


```yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: geoip-db-pv
  namespace: ns-test
spec:
  storageClassName: geoip-db-local-storage # ↑のStorageClassのnameを入れる。
  volumeMode: Filesystem
  capacity:
    storage: 10Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: "/var/lib/k8s-geoip-db" # ローカルの適当なパスを指定
    type: DirectoryOrCreate # ローカルにディレクトリがなければ作ってくれる。
```

describeなどはこんな感じ。


```bash

murata:~ $ kubectl get pv -o wide
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                    STORAGECLASS             REASON   AGE    VOLUMEMODE
geoip-db-pv   10Gi       ROX            Retain           Available                            geoip-db-local-storage            11s    Filesystem
```

```bash

murata:~ $ kubectl describe persistentvolume/geoip-db-pv
Name:            geoip-db-pv
Labels:          <none>
Annotations:     kubectl.kubernetes.io/last-applied-configuration:
                   {"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"geoip-db-pv"},"spec":{"accessModes":["ReadOnlyMany"],"ca...
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    geoip-db-local-storage
Status:          Available
Claim:
Reclaim Policy:  Retain
Access Modes:    ROX
VolumeMode:      Filesystem
Capacity:        10Gi
Node Affinity:   <none>
Message:
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/k8s-geoip-db
    HostPathType:  DirectoryOrCreate
Events:            <none>
```

この時点では`/var/lib/k8s-geoip-db`はできていない。


# PersistentVolumeClaim(PVC)

利用要求をする。


```yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: ns-test
  name: geoip-db-pvc
spec:
  storageClassName: geoip-db-local-storage
  accessModes:
    - ReadOnlyMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 10Gi
```

```bash

murata:~ $ kubectl get pvc -o wide
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS             AGE    VOLUMEMODE
geoip-db-pvc   Bound    geoip-db-pv   10Gi       ROX            geoip-db-local-storage   114s   Filesystem
```


```bash

murata:~ $ kubectl describe persistentvolumeclaim/geoip-db-pvc
Name:          geoip-db-pvc
Namespace:     ns-test
StorageClass:  geoip-db-local-storage
Status:        Bound
Volume:        geoip-db-pv
Labels:        <none>
Annotations:   kubectl.kubernetes.io/last-applied-configuration:
                 {"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"geoip-db-pvc","namespace":"ns-test"},"spec":{"acces...
               pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      10Gi
Access Modes:  ROX
VolumeMode:    Filesystem
Mounted By:    <none>
Events:        <none>
```


# 3点セット確認

StorageClass, pv,pvcをまとめてみるとこんな感じになる。


```bash

murata:~ $ kubectl get sc,pv,pvc -o wide
NAME                                                 PROVISIONER                    AGE
storageclass.storage.k8s.io/geoip-db-local-storage   kubernetes.io/no-provisioner   13m

NAME                           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS             REASON   AGE     VOLUMEMODE
persistentvolume/geoip-db-pv   10Gi       ROX            Retain           Bound    ns-test/geoip-db-pvc     geoip-db-local-storage            7m59s   Filesystem

NAME                                 STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS             AGE     VOLUMEMODE
persistentvolumeclaim/geoip-db-pvc   Bound    geoip-db-pv   10Gi       ROX            geoip-db-local-storage   3m51s   Filesystem
```

```bash

murata:~ $ kubectl describe storageclass.storage.k8s.io/geoip-db-local-storage persistentvolume/geoip-db-pv persistentvolumeclaim/geoip-db-pvc
Name:            geoip-db-local-storage
IsDefaultClass:  No
Annotations:     kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"geoip-db-local-storage"},"provisioner":"kubernetes.io/no-provisioner","reclaimPolicy":"Retain","volumeBindingMode":"Immediate"}

Provisioner:           kubernetes.io/no-provisioner
Parameters:            <none>
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Retain
VolumeBindingMode:     Immediate
Events:                <none>


Name:            geoip-db-pv
Labels:          <none>
Annotations:     kubectl.kubernetes.io/last-applied-configuration:
                   {"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"geoip-db-pv"},"spec":{"accessModes":["ReadOnlyMany"],"ca...
                 pv.kubernetes.io/bound-by-controller: yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    geoip-db-local-storage
Status:          Bound
Claim:           ns-test/geoip-db-pvc
Reclaim Policy:  Retain
Access Modes:    ROX
VolumeMode:      Filesystem
Capacity:        10Gi
Node Affinity:   <none>
Message:
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/k8s-geoip-db
    HostPathType:  DirectoryOrCreate
Events:            <none>


Name:          geoip-db-pvc
Namespace:     ns-test
StorageClass:  geoip-db-local-storage
Status:        Bound
Volume:        geoip-db-pv
Labels:        <none>
Annotations:   kubectl.kubernetes.io/last-applied-configuration:
                 {"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"geoip-db-pvc","namespace":"ns-test"},"spec":{"acces...
               pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      10Gi
Access Modes:  ROX
VolumeMode:    Filesystem
Mounted By:    <none>
Events:        <none>

```

# Podから利用してみる。

コンテナの`/data` ディレクトリを↑で作ったPVCでマウントしてみる。

```yaml

apiVersion: v1
kind: Pod
metadata:
  namespace: ns-test
  name: geoip-pv-test
spec:
  containers:
  - image: alpine
    name: alpine
    command: ["tail", "-f", "/dev/null"]
    volumeMounts:
    - name: claim-volume
      mountPath: /data
  volumes:
  - name: claim-volume
    persistentVolumeClaim:
      claimName: geoip-db-pvc
  terminationGracePeriodSeconds: 0
```


describeしてみるとMountsのところに項目がある。
(ReadOnlyManyにしたのにrwとは・・・？→AccessModeはノード間のアクセスモードでpods間ではないので注意)




```bash


murata:~ $ kubectl describe pods/geoip-pv-test
Name:         geoip-pv-test
Namespace:    ns-test
Priority:     0
Node:         ip-10-0-12-97.ap-northeast-1.compute.internal/10.0.12.97
Start Time:   Wed, 11 Mar 2020 17:26:35 +0900
Labels:       <none>
Annotations:  cni.projectcalico.org/podIP: 192.168.0.125/32
              kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"geoip-pv-test","namespace":"ns-test"},"spec":{"containers":[{"command...
Status:       Running
IP:           192.168.0.125
IPs:
  IP:  192.168.0.125
Containers:
  alpine:
    Container ID:  docker://c88f4ea644a4da03d600eac6fd12ba48a1eebafcdc0dac3d946bb8b8af7f7860
    Image:         alpine
    Image ID:      docker-pullable://alpine@sha256:ab00606a42621fb68f2ed6ad3c88be54397f981a7b70a79db3d1172b11c4367d
    Port:          <none>
    Host Port:     <none>
    Command:
      tail
      -f
      /dev/null
    State:          Running
      Started:      Wed, 11 Mar 2020 17:26:39 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /data from claim-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-2sr2p (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  claim-volume:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  geoip-db-pvc
    ReadOnly:   false
  default-token-2sr2p:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-2sr2p
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age        From                                                    Message
  ----    ------     ----       ----                                                    -------
  Normal  Scheduled  <unknown>  default-scheduler                                       Successfully assigned ns-test/geoip-pv-test to ip-10-0-12-97.ap-northeast-1.compute.internal
  Normal  Pulling    20s        kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal  Pulling image "alpine"
  Normal  Pulled     17s        kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal  Successfully pulled image "alpine"
  Normal  Created    17s        kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal  Created container alpine
  Normal  Started    17s        kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal  Started container alpine
```


# 確認


ディレクトリは勝手に出来上がるので、適当にファイルを保存してみてコンテナ側から参照できるか確認してみましょう。


```bash:ローカル

[root@ip-10-0-12-97 ~]# ll /var/lib | grep k8s-geoip-db
drwxr-xr-x  2 root           root       6 Mar 11 17:26 k8s-geoip-db
[root@ip-10-0-12-97 ~]# echo 'Oreha Worker Node.' > /var/lib/k8s-geoip-db/test.txt
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
```

```sh:コンテナ

murata:/var/lib $ kubectl exec -it geoip-pv-test sh
/ # ls -al /data/
total 4
drwxr-xr-x    2 root     root            22 Mar 11 08:35 .
drwxr-xr-x    1 root     root            41 Mar 11 08:26 ..
-rw-r--r--    1 root     root            19 Mar 11 08:35 test.txt
/ # cat /data/test.txt
Oreha Worker Node.

```

大丈夫できてる。
でもコンテナからでもファイルに追記できてしまった。。。

```sh:コンテナ

/ # echo 'FF7 hayaku!' >> /data/test.txt
/ # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
```

```bash:ローカル
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
FF7 hayaku!
```


ほかのPodからもアクセスできるか確認してみよう。
geoip-pv-test2という名前でpodをもう一つ立てた。


```bash:コンテナ2
murata:~ $ kubectl exec -it pod/geoip-pv-test2 sh
/ # cd /data
/data # ls
test.txt
/data # cat test.txt
Oreha Worker Node.
FF7 hayaku!
/data # echo 'Oreha Cloud no Clone da!' >> /data/test.txt
/data # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
```

```bash:ローカル
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
```

```bash:コンテナ

/ # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
```

共有されいた。


# PVのpathを変えてみる

オンラインの状態でPVのhostPathを挿げ替えたらどうなるだろう。

```
murata:~ $ cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: PersistentVolume
> metadata:
>   name: geoip-db-pv
>   namespace: ns-test
> spec:
>   storageClassName: geoip-db-local-storage
>   volumeMode: Filesystem
>   capacity:
>     storage: 10Gi
>   accessModes:
>     - ReadOnlyMany
>   persistentVolumeReclaimPolicy: Retain
>   hostPath:
>     path: "/var/lib/tmp-k8s-geoip-db" # ここ変える
>     type: DirectoryOrCreate
>
> EOF
The PersistentVolume "geoip-db-pv" is invalid: spec.persistentvolumesource: Forbidden: is immutable after creation
```

怒られまーした。



# まとめ

これでmmdbファイルを取ってきて保存するPodを作って、cronjobで定期実行し、利用するPodsからはそこを参照すればよさそう。

k8sはやっぱり難しいが、それぞれの役割がわかってくればそれほど難しい話ではないのかも？
スナップショットが取れたりクローンできたりもする。

注意点としてはStorageClassによって制約があり、EBSを使った場合には複数からマウントできないなどの制約は出てくるよ。
Pods間でファイルを共有する方法としてほかに、Podから直接EBSなどのボリュームをマウントしたり、サイドカーと言ってPodに複数のコンテナを定義して読めるようにしたりする方法もあるみたい。
PVのpersistentVolumeReclaimPolicyで使い捨て（要求時に毎回空にされるなど）できたりもする模様。

今回の場合、更新Pod以外は読み取り専用でよいのでs3などを利用したほうがよいのかも。。。
逆にMysqlなどのDBのdatadirなどでは使える。(本番ではRDSとか使うんだぞ。)

**注意** ネームスペースをまたいでの多重のPVClaimはできない模様です。同じPATHでpv,pvc,storageClassを作ればいけました。（ファイル壊れる可能性あり得る


参考: [Kubernetes道場 12日目 - PersistentVolume / PersistentVolumeClaim / StorageClassについて](https://cstoku.dev/posts/2018/k8sdojo-12/) (いつも参考にさせていただいています。ありがとう。