KubernetesのPersistentVolumeClaimでNFSを動的プロビジョンする

# 経緯

Kubernetes(k8s)は永続ボリューム（PersistentVolume）に対応していますが、アプリケーション側がこれをコントロールする場合、サーバ上の物理的な情報を意識する必要があります。例えばNFSのPersistentVolumeの定義は以下のように行います。

```
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfsvol1
  annotations:
    volume.beta.kubernetes.io/storage-class: "nfs"
spec:
  capacity:
    storage: 8Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.11.25
    path: /var/share/nfs
```

NFSサーバの接続情報が必要になっています。

k8sを1人や少数で利用する環境ならこれでもいいですが、商用システムの運用を想定すると、基盤系とアプリ系で役割分担や責務が分かれる場合が多いですし、何よりもアプリがサーバの物理的な情報を意識するのは、アプリと基盤の独立性を損なう要因にもなり、k8sの思想にも反します。

そこで、永続ボリューム要求（PersistentVolumeClaim）があります。アプリ側はあらかじめ基盤側で定義されたStorage Class名を指定するだけで、必要なボリュームの動的プロビジョンを行うことができます。

一方、動的プロビジョンを行うためにはそれに対応したストレージを利用する必要があります。具体的には、以下リンクのInternal Provisionerにチェックのついているものにk8sは対応しています。

> https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner

クラウドストレージや比較的新しい分散ファイルシステムには対応していますが、NFSのような気軽なものには対応していないことがわかります。しかし、k8sをパブリッククラウドではなくオンプレミスで利用したいケース（ICP: IBM Cloud Privateなど）では、NFSのような枯れた仕組みを使いたいというニーズもあるはずです。

k8sにはExternal Provisionerという仕組みがあり、上表で対応していないストレージにも対応することが出来ます。今回はNFSのExternal Provisionerを利用して、NFSボリュームのPersistentVolumeClaimに対応してみます。

検証環境
- クラスタ: IBM Cloud Private 2.1.0.2 (Kubernetes 1.9.1)
- NFSサーバ: CentOS 7

# 手順

## External Provisionerの準備

### Provisionerの選定

以下のサイトにさまざまなExternal Provisionerが公開されています。

> https://github.com/kubernetes-incubator/external-storage

NFSに対応したProvisionerはnfsとnfs-clientの2つがあります。違いですが、nfsはWorkerノードにマウントされたNFSのパスをhostPathとしてボリューム定義し、PersistentVolumeClaimに割り当てる方式のようです。nfs-clientはマウントは不要で、NFSサーバのアドレスとパスを指定して直接ボリュームを定義します。

好みの問題かもしれませんが、WorkerノードにわざわざNFSマウントをしたくないので、今回はnfs-clientを使用します。

### Storage Classの作成

以下のようにStorage Classを作成します。provisionerの値は任意のものに変更してください。

```storage-class.yaml
$ kubectl apply -f storage-class.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs
provisioner: lab.hoge.jp/nfs
```

### Deploymentの作成

以下のようにDeploymentを作成します。env.PROVISIONER_NAMEはStorage Classのprovisionerと同じ値を、env.NFS_SERVERとnfs.serverは実際のNFSサーバのアドレスを、env.NFS_PATHとnfs.pathは実際のエクスポートされたディレクトリのパスを指定してください。

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

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      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: lab.hoge.jp/nfs
            - name: NFS_SERVER
              value: icp-nfs
            - name: NFS_PATH
              value: /var/share/nfs
      volumes:
        - name: nfs-client-root
          nfs:
            server: icp-nfs
            path: /var/share/nfs
```

### RBACの設定

ServiceAccountはdefaultではなくこれ用のものを用意するのがいいようです。deployment.yamlで`serviceAccountName: nfs-client-provisioner`として宣言していました。このServiceAccountに権限を付与します。

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

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
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"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
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
```

## テスト

上記サイトにテスト用のyamlも置かれていますので簡単にテストできます。`volume.kubernetes.io/storage-class: "nfs"`は先ほど定義したStorage Class名です。それ以外に環境に依存する情報がないというところがポイントです。



provisionerはdefaultネームスペースに作りましたが、PersistentVolumeClaimは試しに別のネームスペースで作ってみます。成功すればNFSボリューム上にSUCCESSというファイルが作成されます。

```test.yaml
$ kubectl apply -f test.yaml -n teru

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim
  annotations:
    volume.kubernetes.io/storage-class: "nfs"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: gcr.io/google_containers/busybox:1.24
    command:
      - "/bin/sh"
    args:
      - "-c"
      - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim
```

結果を見てみます。

```
$ kubectl get pvc -n teru
NAME         STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-claim   Bound     pvc-075b7858-2f24-11e8-8c09-000c29ebfd54   1Mi        RWX            nfs            22s
```

test-claimというPersistentVolumeClaimの状態がBoundとなり、pvc-〜というVolumeが割り当てられたことがわかります。

```
[NFSサーバ]$ find /var/share/nfs/ | grep pvc-075b
/var/share/nfs/teru-test-claim-pvc-075b7858-2f24-11e8-8c09-000c29ebfd54
/var/share/nfs/teru-test-claim-pvc-075b7858-2f24-11e8-8c09-000c29ebfd54/SUCCESS
```

NFS上にSUCCESSファイルも無事作成されていました。

以上です。