Kubernetes-incubator/external-storage: nfs-clientの検証

#はじめに
今回は[Kubernetes-incubator/external-storage](https://github.com/kubernetes-incubator/external-storage/)にて公開されているKubernetes NFS-Client Provisionerのnfs-clientについて検証します。2019/3時点では、Kubernetes-incubator/external-storageでは、NFS関連として今回検証するnfs-clientとnfsの2つ提供されています。以下に違いを示します。

- nfs-clientは、NFS ClientのProvisionerを提供。NFS Serverは別途用意する必要あり。
- nfsは、Kubernetes上でNFS Serverを提供。ただし、Podが削除されるとデータも削除。

特に今回検証するnfs-clientは、以下のような場合に便利です。

- ブロックストレージを準備するのが面倒
- Pod間でデータをシェアするのではなくブロックストレージのように1Pod:1NFSで利用したい
- NFSでDynamic Provisioningを使いたい (KubernetesのオリジナルのNFS対応(in-tree)では不可)

個人ユースなどで利用する場合においては、最後のDynamic Provisioningを使いたいという方は多いのではないでしょうか。

# 検証
この検証では、Kubernetesとは別にNFS Serverを準備した後、StorageClassで指定されるProvisionerとなるnfs-client-provisionerをKubernetes上へデプロイし、これを利用したStorageClassを作成します。その後、このStorageClassを使ってDynamic Provisioningの動作検証を行います。

## 検証環境
以下に、検証環境を示します。

- NFS Server: macOS (version 10.12.6)
- Kubernetes v1.13.1 (Master x 1, Worker x 2)

## NFS Serverの準備
最初にNFS Serverを準備します。本検証ではmacOS(IP: 192.168.0.11)をNFS Serverとして利用します。まず、NFSで共有するディレクトリを作成します。

```bash
$ sudo mkdir -p /share/kubernetes
$ sudo chmod -R 777 /share/kubernetes
```

次に、NFS Serverの設定ファイル`/etc/exports`ファイルを編集します。

```/etc/exports
/share/kubernetes -alldirs 192.168.0.23 192.168.0.24 192.168.0.25 
```

今回検証するnfs-clientは、上記の共有ディレクトリ配下に`{namespace}-${pvcName}-${pvName}`のディレクトリを自動生成します。
そのため、NFS Serverの設定では共有ディレクトリ配下のサブディレクトリのマウントを許可する`-alldirs`オプションを付与する必要があります。また、上記の設定では、KubernetesのNode以外からマウントできないようにNodeのIPアドレスを指定しています。

次に、Kubernetesの全NodeにNFS Clientのパッケージ(`nfs-common`)をインストールします。
下記は、1台の例ですが、全てのNodeで行います。

```bash
$ ssh 192.168.0.23
$ sudo apt-get update
$ sudo apt-get install -y nfs-common
```

## Provisioner(nfs-client-provisioner)のデプロイ
まず、Provisionerのデプロイ先のNamespaceを作成します。
ここでは`nfs-client-provisioner`のNamespaceを作成します。

```
$ kubectl create ns nfs-client-provisioner
```

次に、RBACをデプロイします。
RBACのManifest(`rbac.yaml`)を以下に示します。

```rbac.yaml
kind: ServiceAccount
apiVersion: v1
metadata:
  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: ["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: nfs-client-provisioner
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
    namespace: nfs-client-provisioner
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
```

上記のManifestをデプロイします。

```
$ kubectl create -f rbac.yaml 
```

次に、Provisionerをデプロイします。
ProvisionerのManifest(`deployment.yaml`)を以下に示します。

```deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
  namespace: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      nodeSelector:
        beta.kubernetes.io/arch: amd64
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            # NFS Serverのホスト指定
            - name: NFS_SERVER
              value: 192.168.0.11
            # 共有ディレクトリ
            - name: NFS_PATH
              value: /share/kubernetes
      volumes:
        - name: nfs-client-root
          nfs:
            # NFS Serverのホスト指定
            server: 192.168.0.11
            # 共有ディレクトリ
            path: /share/kubernetes
```

- .spec.template.spac.containers.env の`NFS_SERVER`の値としてNFS Serverのホスト名(or IPアドレス)を設定します。本検証ではNFS ServerのmacOSのIPアドレス(`192.168.0.11`)を指定しています。
- `NFS_PATH`の値として、NFS Serverで共通した共有ディレクトリ名を指定します。
- .spec.template.spac.volumes.nfsも上記と同様にNFS Serverのホスト名(or IPアドレス)と共有ディレクトリ名を指定します。

上記のManifest(`deployment.yaml`)をデプロイします。

```
$ kubectl create -f deployment.yaml
```

次に、StorageClass(SC)をデプロイします。
以下に、SCのManifest(`storageclass.yaml`)を示します。

```storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs
parameters:
  archiveOnDelete: "false"
```

上記のManifest(`storageclass.yaml`)をデプロイします。

```
$ kubectl create -f storageclass.yaml
```

デプロイしたSCを確認すると、SC(`managed-nfs-storage`)がデプロイされています。

```
$ kubectl get sc
NAME                  PROVISIONER      AGE
managed-nfs-storage   fuseim.pri/ifs   3h24m
```

以上で、nfs-clientのセットアップが完了しました。

## Dynamic Provisioningの動作検証
nfs-clientの動作検証を行います。
nfs-clientによりブロックストレージと同様にDynamic Provisioningが利用できるようになります。
そこで、volumeClaimTemplatesを使ったDynamic Provisioningを行うManifestを使って検証します。Manifest(`nginx.yaml`)を以下に示します。

```nginx.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      storageClassName: managed-nfs-storage
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
```

このManifest(`nginx.yaml`)は、.spec.volumeClaimTemplatesの、storageClassNameにnfs-clientのSC(`managed-nfs-storage`)を指定しています。
Manifest(`nginx.yaml`)をデプロイします。

```
$ kubectl create -f nginx.yaml
```

デプロイされたPod、PVC, PVを確認します。

```
$ kubectl get pod
NAME                            READY   STATUS      RESTARTS   AGE
nginx-0                         1/1     Running     0          64s

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
data-nginx-0   Bound    pvc-1a9b86ad-172e-11e9-951e-080027571559   1Gi        RWO            managed-nfs-storage   31s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS          REASON   AGE
pvc-1a9b86ad-172e-11e9-951e-080027571559   1Gi        RWO            Delete           Bound    default/data-nginx-0   managed-nfs-storage            36s
```

すると、spec.volumeClaimTemplatesで指定したPVCが生成され、これに対応したPVがSC(`managed-nfs-storage`)より自動生成されているのが確認できます。
次にmacOSの共有ディレクトリを確認します。
macOS(NFS Server)上でtreeコマンドで`/share`ディレクトリを確認すると、`default-data-nginx-0-pvc-1a9b86ad-172e-11e9-951e-080027571559`が生成されているのが確認できます。

```
$ tree /share/
/share/
└── kubernetes
    └── default-data-nginx-0-pvc-1a9b86ad-172e-11e9-951e-080027571559
```
以上でDynamic Provisioningの動作検証は終わりです。

## PVC,PVの削除の動作検証
デプロイしたPod, PVC, PVを削除します。
まず、デプロイしたnginxのPodを削除します。

```
$ kubectl delete -f nginx.yaml
```

続いて、PVC、PVを削除します。

```
$ kubectl delete pvc data-nginx-0
```

PVはRECLAIM POLICYに`Delete`が設定されているため、PVCが削除されると自動で削除されます。
ここで、macOS(NFS Server)の共有ディレクトリを確認してみます。

```
$ tree /share/
/share/
└── kubernetes
```

すると、`/share/kubernetes`ディレクトリ配下にあった`default-data-nginx-0-pvc-1a9b86ad-172e-11e9-951e-080027571559`ディレクトリが削除されているのが確認できます。

以上でPVC,PVの削除に関する動作検証は終わりです。
このように、nfs-clientを使うことで、共有された`/share/kubernetes`ディレクトリ配下にDynamic ProvisioningでPVが作成される都度、ディレクトリが自動作成され、PVが削除されると自動削除されます。

# おまけ:StorageClassにDefaultを設定

今回の検証のnfs-clientとは関係ありませんが、SCが増えてくると、優先的に利用するSCにDefault設定しておくと便利です。これにより、PVCなどでSCの指定を省略した場合、Defaultに設定されたSCを選択し使用してくれるようになります。
以下に、SCにDefaultを設定する例を示します。

```
$ kubectl patch storageclass managed-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
```

Default設定されたSCは、Kubectl getコマンドでみると、`(default)`が付与されます。

```
$ kubectl get sc
NAME                            PROVISIONER                    AGE
local-disks                     kubernetes.io/no-provisioner   70d
managed-nfs-storage (default)   fuseim.pri/ifs                 71d
```

# 感想
　今回は、Kubernetes-incubator/external-storageのnfs-clientの動作検証を行いました。
オリジナルのKubernetesのNFS(in-tree)ではDynamic Provisioningが行えないため、使い所が限定されていました。しかし、nfs-clientによりDynamic Provisioningが利用できるようになるため、NFSの利用シーンも広がるのではないでしょうか。個人環境では非常に便利です。ただし、注意しなくてはいけないのは、NFSはやはりNFSです。ブロックストレージのようにraw deviceとしては利用できないです。
　さらに、NFSのバッファサイズはファイルシステム＆NFSプロトコルのデータ転送に適した値となっています。[nfs(5)のmanページ](https://linuxjm.osdn.jp/html/util-linux/man5/nfs.5.html)によると1024バイトがデフォルトになります。HDD中心からSSD(Flash)が主力になりつつあるストレージからみたら、NFSのバッファサイズはパフォーマンスボトルネックの原因にもなります。SSD(Flash)内部のコントローラでは4Kチューニングされている製品が多く、SSD(Flash)を利用する場合は、4K単位でRead/Writeをするのがベターパフォーマンスを発揮できます。
　また、NFSはファイルロック制御を全くもたないブロックストレージとは異なり、ファイルシステム&NFSコントロールのプロトコル層(RPC)でロック制御の機構を持っています。そのため、MySQLなどのリレーショナルデータベースでNFSを使うのは、特別な事情がない限りは避けた方が良いでしょう。[「MySQL Documentation: Using NFS with MySQL」](https://dev.mysql.com/doc/refman/8.0/en/disk-issues.html#disk-issues-nfs)に、NFSに関する注意点がまとまっているので、リレーショナルデータベースでNFSを使う場合には、一読されることをお勧めします。
　DB屋さん&ストレージ屋さんからみたらリレーショナルデータベースでNFSを使うなんてあり得ないと20年以上も言われ続けているコモンセンスとも言えるKnow-Howだと思います。しかし、コンテナの普及によりリレーショナルデータベースが簡単に構築できるようになってきた今日では、下位のインフラを気にしなくても簡単に動いてしまうのです。そのためこのような下位のインフラに付随するコモンセンスは、コモンセンスでなくなってきたと感じます。
　ストレージやデータベースについて、ディープな内容を書きましたがNFSは便利です。便利な反面、注意点も多いことも理解した上で利用するのが良いです。特に、StorageClassを準備するKubernetesのクラスタ管理者は、コンテナをデプロイするユーザが安全に利用できるようStorageClassのDefault設定などをうまく使い、下位のインフラのKnow-Howを隠蔽しつつ安全にユーザに使わせるのが良いのではないでしょうか。