【KubernetesのPersistentVolume】その２- Dynamic Provisionerを使ってNFSサーバーをPodにmountする

## 概要
KubernetesでPersistentVolume（永続ストレージ）のDynamic Provisionerを使って、外部ストレージをコンテナにmountする。

Dynamic Provisionerを使わない方法については以下を参照。
このページを読む前に、以下の内容を理解しておいたほうがスムーズかと思われる。
[【KubernetesのPersistentVolume】その１- 外部ストレージをPodにmountする](https://qiita.com/Esfahan/items/7ad7695ea78c7630239a)

## 目次
- [Kubernetesハンズオン目次](https://qiita.com/Esfahan/items/4fcf51622fed20dffb88)

## 前提知識
### Dynamic Provisionerとは
Dynamic Provisionerを使うことで、PersistentVolumeを動的に生成することが可能となる。
そのため、予めPersistentVolumeを作成（定義）しておく必要がない。

具体的な設定方法の話をすると、PersistentVolumeを定義せずに、PersistentVolumeClaimを定義するだけで、Volumeを生成しPodにmountすることができる。

1. PersistentVolumeClaimを定義
1. Podを定義し、PersistentVolumeClaimと関連付ける
1. Podを起動 -> Volumeが自動生成されmountされる

### Provisionerについて
Dynamic Provisionerでmountするには、Provisionerというものを利用する。
このProvisionerはいくつかの種類があり、ストレージの種類ごと(AWS, GCPなど)にそれぞれ使い分ける。

また、デフォルトでいくつかのProvisionerが用意されている。
利用可能なPorvisionerは以下を参照。
https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner

ただし、今回利用するNFSサーバーには、デフォルトのporvisionerが用意されていない。
そこで、external provisionersというものを利用する。

### external provisionersとは
[external provisioners](https://github.com/kubernetes-incubator/external-storage)は、デフォルトで用意されていないProvisionerをカバーするもので、NFS用のものも用意されている。今回はここで用意されている[nfs-client](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client)を利用する。

### external provisionersの仕組み
external provisionersの仕組みを簡単に説明すると、NFSの場合は、[nfs-client](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client)の下の[nfs-client/cmd/nfs-client-provisioner/provisioner.go](https://github.com/kubernetes-incubator/external-storage/blob/master/nfs-client/cmd/nfs-client-provisioner/provisioner.go)によって作られるDockerコンテナ([nfs-client/docker/x86_64/Dockerfile](https://github.com/kubernetes-incubator/external-storage/blob/master/nfs-client/docker/x86_64/Dockerfile))が、NFSのmountなどをやってくれるprovisionerとして動作してくれるようである。

### mountされる仕組み
Provisionerを使ったmountの仕組みを理解するのに少し時間がかかったが、以下の用であると理解した。

1. nfs-provisioner用のPodを作成
1. サービス用のPodを作成
1. サービス用Podがnfs-provisioner用のPodを介してNFSサーバーをmountする

## ハンズオン
それでは実際に環境構築をする。

### 環境
- CentOS 7.6
- Minikube v1.1.1
- Kubernetes v1.14.3

ここではMinikubeで構築した環境のPodに、NFSサーバーをmountする。
**関連記事：**[Minikubeを使ってローカルにkubernetes環境を構築](https://qiita.com/Esfahan/items/f5c846088281c39f73a4)

### NFSサーバーの構築
まずはmountするためのNFSサーバーを構築する。
手順については以下を参照。

- [CentOS7にNFSサーバーを秒で構築](https://qiita.com/Esfahan/items/1b5b3a74dcd86fd30e5c)

### nfs-utilsのインストール
クラスタのホストOS（ここではminikube）に以下をインストール。

```shell-session
$ sudo yum install -y rpcbind nfs-utils
```

## nfs-provisionerの作成
external provisionersを利用して、nfs-provisionerを作成する。

### StorageClass
ストレージの種類を定義する。
`provisioner`という箇所があるが、デフォルトで用意されているProvisionerを利用する場合は、[こちら](https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner)からmountするストレージに合ったProvisionerを指定する。

- e.g.
    - AWSの場合：`provisioner: kubernetes.io/aws-ebs`
    - Azureの場合：`provisioner: kubernetes.io/azure-file`

ただし、今回の用にexternal provisionersを利用する場合は、何でも良いので適当に名前を付ける。
そして、後述のDeploymentの定義のspec.template.spec.containers.env.nameのvalueと同じ名前にして、関連付けられるようにする。

```yaml:storage-class.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  # persitent-volumne-claim.ymlのspec.storageClassNameと合わせる
  name: nfs
# deployment.ymlのspec.template.spec.containers.env.nameのvalueと合わせる
provisioner: lab.hoge.jp/nfs
# Volumeが削除されたときの挙動を定義
# Delete or Retain。デフォルトはDelete。
reclaimPolicy: Retain
```

### PersistentVolumeClaim
PersistentVolumeClaimで、Podが要求するVolumeのspecを定義する。
Dynamic Provisionerを利用する場合は、Volumeは動的に生成されるため、PersistentVolumeを定義する必要はない。

参考：[VolumeとPersistentVolumeとPersistentVolumeClaimの違い](https://thinkit.co.jp/article/14195)

```yaml:persitent-volumne-claim.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  # PVC(PersistentVolumeClaim)のための任意の名前を指定
  name: sample-pvc
  annotations:
    # storage-class.ymlのmetadata.nameと同じ値
    volume.kubernetes.io/storage-class: "nfs"
spec:
  # アクセス権を設定
  accessModes:
    - ReadWriteMany
  # storage-class.ymlのmetadata.nameと合わせる
  # storageClassNameを省略するとHostPathでホストOS上にmountされる
  storageClassName: nfs
  resources:
    requests:
      # 割り当てるstorageの容量
      storage: 1Gi
```

### RBAC
RBAC(Role-based access control)とは、役割ベースのアクセス制御機能。
NFSのprovisionerに与える権限を定義する。
参考：[KubernetesのRBACについて](https://qiita.com/sheepland/items/67a5bb9b19d8686f389d)

```yaml:rbac.yml
kind: ServiceAccount
apiVersion: v1
metadata:
  # pod.ymlのspec.serviceAccountNameと合わせる
  name: nfs-client-provisioner

---
# 許可する操作を定義
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - 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: ["list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

---
# RoleもしくはClusterRoleを関連付けるUserAccountもしくはServiceAcctountを定義
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
```

### Deployment
nfs-porovisionerのDeploymentの定義。

```yaml:deployment.yml
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  # https://qiita.com/MahoTakara/items/a776286e5c4385662df4#%E3%82%B9%E3%83%88%E3%83%A9%E3%83%86%E3%82%B8
  # 古いポッドを新しいポッドに置き換えるための方法
  # defaultはRollingUpdate
  # Recreate 既存のポッドはすべて削除されます。
  strategy:
    type: Recreate
  # nfs-provisionerのPodの定義
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      # https://tech.uzabase.com/entry/2018/03/14/200512
      # これを指定することで、全てのリソースに対する参照all-readerができるようにしています。
      # ちなみに、サービスアカウントを作成するとsecretにca.crt、tokenというデータが作成されます。
      # このsecretはポッドが起動した際に、自動的に/var/run/secrets/kubernetes.io/serviceaccount の配下にマウントされます。
      serviceAccountName: nfs-client-provisioner
      # nfs-provisionerのコンテナの定義
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          # コンテナ内のmountの定義
          volumeMounts:
            # 以下のspec.template.spec.volumes.nameと合わせる
            - name: nfs-client-root
              # コンテナ内でNFSをmountさせるPATH
              mountPath: /persistentvolumes
          env:
            # storage-class.ymlのprovisionerと同じ値にする
            - name: PROVISIONER_NAME
              value: lab.hoge.jp/nfs
            # NFSサーバーのアドレス
            - name: NFS_SERVER
              value: xxx.xxx.xxx.xxx
            # NFSサーバーのmountするPATH
            - name: NFS_PATH
              value: /var/share/nfs
      # コンテナ外のmountの定義
      volumes:
        - name: nfs-client-root
          nfs:
            # NFSサーバーのアドレス
            server: xxx.xxx.xxx.xxx
            # NFSサーバーのmountするPATH
            path: /var/share/nfs
```

### nfs-provisionerを作成
これでnfs-provisioner用のPodが作成される。

```shell-session
$ sudo kubectl apply -f storage-class.yml \
                     -f persitent-volumne-claim.yml \
                     -f rbac.yml \
                     -f deployment.yml
storageclass.storage.k8s.io/nfs created
persistentvolumeclaim/sample-pvc created
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
deployment.extensions/nfs-client-provisioner created
```

### nfs-provisioner作成後確認
#### Pod, Deployment, ReplicaSetを確認
nfs-provisionerのPod, Deployment, ReplicaSetが作成されたことを確認。

```diff
$ sudo kubectl get all
NAME                                          READY   STATUS    RESTARTS   AGE
+pod/nfs-client-provisioner-657f86599c-xhfsm   1/1     Running   0          11s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   97m

NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
+deployment.apps/nfs-client-provisioner   1/1     1            1           11s

NAME                                                DESIRED   CURRENT   READY   AGE
+replicaset.apps/nfs-client-provisioner-657f86599c   1         1         1       11s
```


#### StorageClass, PersistentVolumeClaim, PersistentVolumeを確認
以下の用になっていることを確認

- StorageClassが作成された。
- PersistentVolumeClaimは作成されているがSTATUSがPendingのまま。
- PersistentVolumeは作成されていない。

```diff
$ sudo kubectl get sc,pv,pvc
NAME                                             PROVISIONER                AGE
+storageclass.storage.k8s.io/nfs                  lab.hoge.jp/nfs            4m9s
storageclass.storage.k8s.io/standard (default)   k8s.io/minikube-hostpath   19m

NAME                               STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
+persistentvolumeclaim/sample-pvc   Pending                                      nfs            4m9s
```

#### mountされているか確認
nfs-provisionerコンテナ内から、NFSサーバーのmountしたディレクトリが見えるか確認。

NFSサーバーのmountする場所にテスト用ファイルを作成する。

```shell-session
$ sudo echo 'This is a sample.' > /var/share/nfs/sample.txt
```

作成されたnfs-client-provisionerのPodのNFS_PATHの中を確認すると、sample.txtが見える。

```shell-session
$ sudo kubectl exec -ti nfs-client-provisioner-657f86599c-xhfsm ls /persistentvolumes
sample.txt

$ sudo kubectl exec -ti nfs-client-provisioner-657f86599c-xhfsm cat /persistentvolumes/sample.txt
This is a sample.
```

これでnfs-provisionerの作成は完了。

## Podを作成してNFSサーバーをmountする
次に、Podを作成して、nfs-provisionerを介してNFSサーバーの領域をmountする。

### NFSサーバーのmountされるディレクトリのpermissionを変更
nfs-provisionerが、NFSサーバーのmountディレクトリの下に、`nfsnobody`というユーザーでディレクトリを作成するため、権限を与える必要がある。

```shell-session
$ sudo chmod 777 /var/share/nfs
```

権限を与えないと、mount時にmkdirできずに以下のエラーが発生する。

```shell-session
$ sudo kubectl describe persistentvolumeclaim/sample-pvc
# **snip**
Events:
  Type       Reason                Age                   From                                                                                          Message
  ----       ------                ----                  ----                                                                                          -------
  Normal     Provisioning          101s (x4 over 3m26s)  lab.hoge.jp/nfs_nfs-client-provisioner-657f86599c-5mp5r_91c30fd0-922e-11e9-9954-0242c0a8f904  External provisioner is provisioning volume for claim "default/sample-pvc"
  Warning    ProvisioningFailed    101s (x4 over 3m26s)  lab.hoge.jp/nfs_nfs-client-provisioner-657f86599c-5mp5r_91c30fd0-922e-11e9-9954-0242c0a8f904  
failed to provision volume with StorageClass "nfs": unable to create directory to provision new pv: 
mkdir /persistentvolumes/default-sample-pvc-pvc-8ff59f6d-922e-11e9-afb4-fa163e42f6bd: permission denied

  Normal     ExternalProvisioning  9s (x16 over 3m29s)   persistentvolume-controller                                                                   waiting for a volume to be created, either by external provisioner "lab.hoge.jp/nfs" or manually created by system administrator
Mounted By:  <none>
```

### Podを定義

```yaml:pod.yml
kind: Pod
apiVersion: v1
metadata:
  name: nginx-pod
spec:
  # rbac.ymlのmetadata.nameと合わせる
  # これでRBACと関連付けられる
  serviceAccountName: nfs-client-provisioner
  containers:
  # Dockerコンテナの名前
  - name: nginx-container
    # 利用するDockerイメージ
    image: nginx
    # 公開するDockerコンテナのポート
    ports:
      - containerPort: 80
    # Dockerコンテナ内のmountポイント
    volumeMounts:
      # 以下のspec.volumes.nameと合わせる
      - name: nfs-pvc
        mountPath: "/mnt"
  # Never or OnFailure
  restartPolicy: OnFailure
  # mountする外部ストレージの設定
  volumes:
    # 上記のspec.containers.volumeMounts.nameと合わせる
    - name: nfs-pvc
      persistentVolumeClaim:
        # persitent-volumne-claim.ymlのmetadatal.nameと合わせる
        # これでPersistentVolumeClaimと関連付けられる
        claimName: sample-pvc
```

### Podを作成

```shell-session
$ sudo kubectl apply -f pod.yml
pod/nginx-pod created
```

### Pod確認
nginx-podというPodが作成されたことを確認。

```diff
$ sudo kubectl get pod
NAME                                      READY   STATUS    RESTARTS   AGE
nfs-client-provisioner-657f86599c-xhfsm   1/1     Running   0          19m
+nginx-pod                                 1/1     Running   0          13s
```

### PersistentVolumeClaim, PersistentVolumeを確認
以下の用になっていることを確認。

- PersistentVolumeClaimのSTATUSがBoundになった。
- PersistentVolumeが作成された。

```diff
$ sudo kubectl get pvc,pv
NAME                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
+persistentvolumeclaim/sample-pvc   Bound    pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd   1Gi        RWX            nfs            20m

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
+persistentvolume/pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd   1Gi        RWX            Retain           Bound    default/sample-pvc   nfs                     20m
```

describeすることでどこにmountされているのか分かる。

```shell-session
$ sudo kubectl describe persistentvolume/pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd
Name:            pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd
Labels:          <none>
Annotations:     pv.kubernetes.io/provisioned-by: lab.hoge.jp/nfs
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    nfs
Status:          Bound
Claim:           default/sample-pvc
Reclaim Policy:  Retain
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        1Gi
Node Affinity:   <none>
Message:
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    xxx.xxx.xxx.xxx
    Path:      /var/share/nfs/default-sample-pvc-pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd
    ReadOnly:  false
Events:        <none>
```

### mountされているか確認
#### NFSサーバーでの作業
NFSサーバーに以下のディレクトリが作成されている。

```shell-session
$ ls -l /var/share/nfs/
drwxrwxrwx 2 nfsnobody nfsnobody  6 Jun 19 14:33 default-sample-pvc-pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd
```

作成されたディレクトリの下にテスト用ファイルを作成。

```shell-session
$ cd /var/share/nfs/default-sample-pvc-pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd
$ echo 'This is a sample in PersistenVolume.' > persistent-volume.txt
```

#### Podを確認
Pod内のmountされたディレクトリを確認する。
NFSサーバーの`/var/share/nfs/default-sample-pvc-pvc-b3ee5521-9253-11e9-b851-fa163e42f6bd`の中と同じファイルが見えていればOK。

```shell-session
$ sudo kubectl exec -it nginx-pod ls /mnt
persistent-volume.txt

$ sudo kubectl exec -it nginx-pod cat /mnt/persistent-volume.txt
This is a sample in PersistenVolume.
```

## 関連記事
- [【KubernetesのPersistentVolume】その１- 外部ストレージをPodにmountする](https://qiita.com/Esfahan/items/7ad7695ea78c7630239a)

## 参考
- [Kubernetes NFS-Client Provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client)
- [Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/)
- [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
- [KubernetesのPersistentVolumeClaimでNFSを動的プロビジョンする](https://qiita.com/teruq/items/17609eecc29b70e2cbe7)
- [Kubernetes-incubator/external-storage: nfs-clientの検証](https://qiita.com/ysakashita/items/cbfa1403fcd83121a397)
- [Kubernetes: Local Volumeの検証](https://qiita.com/ysakashita/items/67a452e76260b1211920)
- [Kubernetes道場 12日目 - PersistentVolume / PersistentVolumeClaim / StorageClassについて](https://cstoku.dev/posts/2018/k8sdojo-12/)