KubernetesにおけるContainer Storage Interface (CSI)の概要と検証

# CSIの概要
　Kubernetes v1.9より***Container Storage Interface*** (CSI)がAlphaバージョンとしてサポートが開始され、v1.10にてBetaバージョンとしてサポートされています。v1.13でGAになりました。v1.8 までのKubernetesのストレージ関連の機能は、Kubernetesのソースに直に組み込まれる実装("in-tree")で提供されていました。そのため、外部ストレージの開発を行う3rd パーティベンダは、Kubernetes のソースコードへアップストリームする必要があり、リリースのタイミングをKubernetesの開発チームと歩調をあわせる必要がありました。また、v1.8にて追加された[Flexvolume](https://github.com/kubernetes/community/blob/master/contributors/devel/flexvolume.md)では、Kubernetesの Node, MasterのRoot FileSystem にアクセスが必要であり、デプロイが煩雑でした。
　Kubernetesでは、CSIをサポートすることで、Kubernetesのソースに組み込まず3rdパーティベンダが独自に実装できる("out-of-tree")にて提供することが可能となりました。さらに、Kubernetesでは以前より備えているStorageClass, PersistentVolume, PersistentVolumeClaimを引き続きCSIでも使うことで、コンテナをデプロイしようとするユーザから見て、v1.8以前と同様の操作でCSI経由で外部ストレージを利用することが可能となります。
　このCSIは，Kubernetes, Mesos, Docker, CloudFoundry など幅広くコンテナ環境で利用できるようにすべく、Kubernetesとは独立し、[Container Storage Interface コミュニティ](https://github.com/container-storage-interface)にて仕様が策定されています。

## KubernetesにおけるCSIのアーキテクチャ
KubernetesにおけるCSIのアーキテクチャ図を示します。
![csi-arch.png](https://qiita-image-store.s3.amazonaws.com/0/229880/34cafde0-4fa2-d58a-4f7f-02d020b3ec33.png)

3rdパーティベンダは、Kubeletとやり取りを行うNode Pluginと、API Serverからのリクエストに対応するController Pluginの2つのCSI Volume Driverを用意する必要があります。
Node PluginはKubelet毎(Node毎)に1つのPodが必要なため、DaemonSet Podとして構築します。
Controller Pluginは、Nodeに縛られずKubernetes Cluster上に存在すれば良いので、StatefulSetにて構築します。

## CSI Volumeの状態遷移
次に、CSI Volumeの状態について状態遷移図を用い説明します。
Kubernetesでは、Controller PluginからのリクエストにてCSI Volumeの状態が遷移します。
![csi-vol.png](https://qiita-image-store.s3.amazonaws.com/0/229880/1182189d-9ddf-6434-d551-a10a4c974462.png)
まず、`CreateVolume`リクエストによりCSI Volumeが作成されます。その後、NODE_READY, VOL_READYの状態を経て、Pod(Container)が利用できる状態のPUBLISHEDへ状態遷移します。
CSIでは、この状態遷移に対応したAPIが定義されています。この状態遷移図にないAPIは、`ListVolumes`などの参照系のAPIになります。
また、上記に示した状態遷移が基本となっていますが、3rdパーティベンダの実装によっては、NODE_READY, VOL_READYが省略されるケースもあるようです。

## KubernetesでCSIを使うための設定
v1.9ではCSIはAlphaバージョンのため、以下のフラグを設定する必要があります。

- API server binary
  - --feature-gates=CSIPersistentVolume=true
  - --runtime-config=storage.k8s.io/v1alpha1=true
- API server binary and kubelet binaries
  - --feature-gates=MountPropagation=true
  - --allow-privileged=true

v1.10ではCSIはBetaバージョンのため、feature-gates, runtime-configの設定は不要です。

# 検証
検証は、以下を用いて行います。
- Kubernetes v1.10 (kubeadmを使いMacのVirtualBox上に構築)
- [CSI Drivers](https://github.com/kubernetes-csi/drivers)
- 外部ストレージ: NFS Server(Mac macOS(10.12.6)のNFS Serverを利用)

※ kubeadmを使ったKubernetes v1.10の構築手順については割愛します。kubeadmを使った構築は、[こちらのドキュメント](https://kubernetes.io/docs/setup/independent/install-kubeadm/)を参照ください。

## 外部ストレージの準備
まず初めに、外部ストレージとしてNFS Serverを準備します。
共有するディレクトリを作成し、NFS Serverの設定ファイル(/etc/exports)を作成します。
今回は、192.168.0.0/24のネットワーク上で検証を行うため，下記例では192.168.0.0 のネットワークからのアクセスを許可しています．

```
$ sudo mkdir /share
$ sudo chmod 777 /share
$ sudo vi /etc/exports
/share -mapall=nobody:wheel -network 192.168.0.0 -mask 255.255.255.0
```
次に，NFS Serverのデーモンを起動します.

```
$ sudo nfsd start
$ sudo nfsd update  
$ sudo showmount -e  
Exports list on localhost:
/share                              192.168.0.0
```

NFSでシェアしたディレクトリがKubernetesのKubeletが動作するNodeのマシンから利用できるか確認します。
KubernetesのNodeのマシンにSSHなどでログインし、シェアしたディレクトリをマウントしてみます。

```
$ sudo mount -t nfs 192.168.0.11:/share /mnt
```
上記の192.168.0.11は母艦のmacOSのIPアドレスです。各自の環境に合わせて変更してください。
もし、mount コマンドにて以下のエラーが出る人は、NodeのマシンにNFS Clientがインストールされていない可能性がありますので、apt-getコマンドなどを使いNFS Clientをインストールしてください(筆者はしばしハマりました)。

```
$ sudo mount -t nfs 192.168.0.11:/share /mnt
mount: wrong fs type, bad option, bad superblock on 192.168.0.11:/share,
       missing codepage or helper program, or other error
       (for several filesystems (e.g. nfs, cifs) you might
       need a /sbin/mount.<type> helper program)

       In some cases useful info is found in syslog - try
       dmesg | tail or so.

$ sudo apt-get update
$ sudo apt-get install nfs-common -y
```

df コマンドを使い、NFSでシェアされたディレクトリ/shareが/mntにマウントされているのを確認します。

```
$ df -h
Filesystem                Size  Used Avail Use% Mounted on
<snip>
192.168.0.11:/share       931G  524G  408G  57% /mnt

```
NodeのマシンからNFS Serverでシェアしたディレクトリが利用できることが確認できたので、マウントを外しておきます。

```
$ sudo umount /mnt
$ exit
```

## CSI Driversのダウンロード
本検証では、利用するCSIとして、CSIの実装サンプルとして提供している[CSI Drivers](https://github.com/kubernetes-csi/drivers)を利用します。

```
$ git clone https://github.com/kubernetes-csi/drivers.git
$ cd drivers/pkg/nfs/deploy/kubernetes
```
以降、`drivers/pkg/nfs/deploy/kubernetes`ディレクトリで作業を行います。

## CSI Volume Driver Container(Node Plugin)の構築
`drivers/pkg/nfs/deploy/kubernetes`ディレクトリにあるNode Pluginの構成定義`csi-nodeplugin-nfsplugin.yaml`と、そのRBACの設定定義`csi-nodeplugin-rbac.yaml`を使って構築します。
以下に、Node Pluginの構成定義を示します。

```csi-nodeplugin-nfsplugin.yaml
kind: DaemonSet
apiVersion: apps/v1beta2
metadata:
  name: csi-nodeplugin-nfsplugin
spec:
  selector:
    matchLabels:
      app: csi-nodeplugin-nfsplugin
  template:
    metadata:
      labels:
        app: csi-nodeplugin-nfsplugin
    spec:
      serviceAccount: csi-nodeplugin
      hostNetwork: true
      containers:
        - name: driver-registrar
          image: quay.io/k8scsi/driver-registrar:v0.2.0
          args:
            - "--v=5"
            - "--csi-address=$(ADDRESS)"
          env:
            - name: ADDRESS
              value: /plugin/csi.sock
            - name: KUBE_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          volumeMounts:
            - name: plugin-dir
              mountPath: /plugin
        - name: nfs
          securityContext:
            privileged: true
            capabilities:
              add: ["SYS_ADMIN"]
            allowPrivilegeEscalation: true
          image: quay.io/k8scsi/nfsplugin:v0.2.0
          args :
            - "--nodeid=$(NODE_ID)"
            - "--endpoint=$(CSI_ENDPOINT)"
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CSI_ENDPOINT
              value: unix://plugin/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: plugin-dir
              mountPath: /plugin
            - name: pods-mount-dir
              mountPath: /var/lib/kubelet/pods
              mountPropagation: "Bidirectional"
      volumes:
        - name: plugin-dir
          hostPath:
            path: /var/lib/kubelet/plugins/csi-nfsplugin
            type: DirectoryOrCreate
        - name: pods-mount-dir
          hostPath:
            path: /var/lib/kubelet/pods
            type: Directory
```
Node Pluginは、Kubelet毎(Node毎)に必要となるため、Node毎に1つづつデプロイされるようにDaemonSetを使って構築します。また、UDS(Unix Domain Socket)のunix://plugin/csi.sockおよび/var/lib/kubeletのVolumeを使いKubeletとやり取りを行います。

kubectlコマンドを用い、上記のNode Pluginと、そのRBAC設定定義をデプロイします。

```
$ kubectl create -f csi-nodeplugin-nfsplugin.yaml
daemonset.apps "csi-nodeplugin-nfsplugin" created

$ kubectl create -f csi-nodeplugin-rbac.yaml 
serviceaccount "csi-nodeplugin" created
clusterrole.rbac.authorization.k8s.io "csi-nodeplugin" created
clusterrolebinding.rbac.authorization.k8s.io "csi-nodeplugin" created
```
Node Pluginがデプロイされていることを確認します。

```
$ kubectl get ds
NAME                       DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
csi-nodeplugin-nfsplugin   1         1         1         1            1           <none>          1m
```

## CSI Volume Driver Container(Controller Plugin)の構築
続いて、Controller Pluginを、Controller Pluginの構成定義`csi-attacher-nfsplugin.yaml`とそのRBAC設定定義`csi-attacher-rbac.yaml`を使って構築します。
以下に、Controller PluginとアクセスするためのServiceの構成定義を示します。

```csi-attacher-nfsplugin.yaml
kind: Service
apiVersion: v1
metadata:
  name: csi-attacher-nfsplugin
  labels:
    app: csi-attacher-nfsplugin
spec:
  selector:
    app: csi-attacher-nfsplugin
  ports:
    - name: dummy
      port: 12345

---
kind: StatefulSet
apiVersion: apps/v1beta1
metadata:
  name: csi-attacher-nfsplugin
spec:
  serviceName: "csi-attacher"
  replicas: 1
  template:
    metadata:
      labels:
        app: csi-attacher-nfsplugin
    spec:
      serviceAccount: csi-attacher
      containers:
        - name: csi-attacher
          image: quay.io/k8scsi/csi-attacher:v0.2.0
          args:
            - "--v=5"
            - "--csi-address=$(ADDRESS)"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/

        - name: nfs
          image: quay.io/k8scsi/nfsplugin:v0.2.0
          args :
            - "--nodeid=$(NODE_ID)"
            - "--endpoint=$(CSI_ENDPOINT)"
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CSI_ENDPOINT
              value: unix://plugin/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /plugin
      volumes:
        - name: socket-dir
          emptyDir:
```
Controller Pluginでは、socket-dirとしてemptyDirをマウントし、UDSのunix://plugin/csi.sockを経由し、csi-attacherとやり取りを行います。

kubectlコマンドを用い、上記のController PluginとService及び、RBAC設定定義をデプロイします。

```
$ kubectl create -f csi-attacher-nfsplugin.yaml 
service "csi-attacher-nfsplugin" created
statefulset.apps "csi-attacher-nfsplugin" created

$ kubectl create -f csi-attacher-rbac.yaml
serviceaccount "csi-attacher" created
clusterrole.rbac.authorization.k8s.io "external-attacher-runner" created
clusterrolebinding.rbac.authorization.k8s.io "csi-attacher-role" created
```
Controller Pluginと、アクセスするためのServiceがデプロイされていることを確認します。

```
$ kubectl get sts
NAME                     DESIRED   CURRENT   AGE
csi-attacher-nfsplugin   1         1         1m

$ kubectl get svc
NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
csi-attacher-nfsplugin   ClusterIP   10.110.73.102   <none>        12345/TCP   3m
kubernetes               ClusterIP   10.96.0.1       <none>        443/TCP     4d
```
ここまでのデプロイで、CSI を使うためのCSI Volume Driverの設定が完了しました。

## CSI Volumeを使ったPodの構築
次は、デプロイしたCSI Volume Driverを通じ、PodからNFS Serverにてシェアされたディレクトリを利用します。
サンプルのPodの定義として、[CSI Drivers](https://github.com/kubernetes-csi/drivers)が用意しているnginxの構成定義を使います。

```
$ /home/sakasita/csi/drivers/pkg/nfs/examples/kubernetes
$ vi nginx.yaml
```
サンプルが保存されているディレクトリへ移動後、viなどのエディタを用い`nginx.yaml`を編集します。
編集箇所を以下に示します。

```
$ git diff
diff --git a/pkg/nfs/examples/kubernetes/nginx.yaml b/pkg/nfs/examples/kubernetes/nginx.yaml
index 8048e9c..28a9c23 100644
--- a/pkg/nfs/examples/kubernetes/nginx.yaml
+++ b/pkg/nfs/examples/kubernetes/nginx.yaml
@@ -13,8 +13,8 @@ spec:
     driver: csi-nfsplugin
     volumeHandle: data-id
     volumeAttributes: 
-      server: 127.0.0.1
-      share: /export
+      server: 192.168.0.11
+      share: /share
 ---
 apiVersion: v1
 kind: PersistentVolumeClaim
```
`volumeAttributes`に母艦のmacOS上のNFS Serverでシェアしたディレクトリを指定します。
変更後の`nginx.yaml`を以下に示します。`nginx.yaml`ではCSIを使うためのPersistenvVolume, PersistentVolumeClaimおよびnginxの構成定義が含まれています。


```nginx.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: data-nfsplugin
  labels:
    name: data-nfsplugin
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 100Gi
  csi:
    driver: csi-nfsplugin
    volumeHandle: data-id
    volumeAttributes: 
      server: 192.168.0.11
      share: /share
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-nfsplugin
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  selector:
    matchExpressions:
    - key: name
      operator: In
      values: ["data-nfsplugin"]
---
apiVersion: v1
kind: Pod
metadata:
  name: my-nginx 
spec:
  containers:
  - image: maersk/nginx
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
    volumeMounts:
      - mountPath: /var/www
        name: data-nfsplugin 
  volumes:
  - name: data-nfsplugin
    persistentVolumeClaim:
      claimName: data-nfsplugin 
```
kubectlコマンドにて上記PersistenvVolume, PersistentVolumeClaimおよびnginxをデプロイします。

```
$ kubectl create -f nginx.yaml 
persistentvolume "data-nfsplugin" created
persistentvolumeclaim "data-nfsplugin" created
pod "nginx" created
```

## 確認
CSIを利用し、NFS ServerでシェアされたディレクトリがPodでマウントされていることを確認します。
まず、PersistentVolumeClaim, PersistentVolumeを確認します。

```
$ kubectl get pvc
NAME             STATUS    VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-nfsplugin   Bound     data-nfsplugin   100Gi      RWX                           8m

$ kubectl get pv
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                    STORAGECLASS   REASON    AGE
data-nfsplugin   100Gi      RWX            Retain           Bound     default/data-nfsplugin                            8m

$ kubectl describe pv data-nfsplugin
Name:            data-nfsplugin
Labels:          name=data-nfsplugin
Annotations:     pv.kubernetes.io/bound-by-controller=yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    
Status:          Bound
Claim:           default/data-nfsplugin
Reclaim Policy:  Retain
Access Modes:    RWX
Capacity:        100Gi
Node Affinity:   <none>
Message:         
Source:
    Type:          CSI (a Container Storage Interface (CSI) volume source)
    Driver:        csi-nfsplugin
    VolumeHandle:  data-id
    ReadOnly:      false
Events:            <none>
```
PersistetVolumeClaim, PersistentVolumeが正しくデプロイされているのが確認できます。また、kubectl describeで作成されたPersistentVolumeを見てみると、Source.TypeにてCSIのVolumeであることが確認できます。
続いて、nginxのPodを確認します。

```
$ kubectl get pods 
NAME                             READY     STATUS    RESTARTS   AGE
csi-attacher-nfsplugin-0         2/2       Running   0          1h
csi-nodeplugin-nfsplugin-4k924   2/2       Running   0          1h
nginx                            1/1       Running   0          17m

$ kubectl exec -ti nginx /bin/bash
root@nginx:/# df -h
Filesystem                Size  Used Avail Use% Mounted on
none                       30G  4.8G   24G  17% /
tmpfs                     2.0G     0  2.0G   0% /dev
tmpfs                     2.0G     0  2.0G   0% /sys/fs/cgroup
/dev/mapper/k8s--vg-root   30G  4.8G   24G  17% /etc/hosts
192.168.0.11:/share       931G  523G  408G  57% /var/www
shm                        64M     0   64M   0% /dev/shm
tmpfs                     2.0G   12K  2.0G   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs                     2.0G     0  2.0G   0% /sys/firmware
```
nginxのPodにログインし、dfコマンドで見てみると、NFS Serverでシェアされた/shareディレクトリが、正しくマウントされているのが確認できます。
このように、CSIを通じて外部ストレージを利用する際、PersistentVolumeはCSIであることを意識しますが、PersistentVolumeClaim, Podの構成定義では意識しません。
これにより、Podを作成するKubernetesの利用者(ユーザ)は、CSIかどうかを意識することなく、Kuberneetesの管理者が定義したPersistentVolumeを利用するだけで、外部ストレージを利用できます。

## クリーンアップ
検証にてデプロイしたPersistentVolume,PersistentVolumeClaim,nginxのPodを削除します。

```
$ kubectl delete -f nginx.yaml 
persistentvolume "data-nfsplugin" deleted
persistentvolumeclaim "data-nfsplugin" deleted
pod "nginx" deleted
```
次に、CSI Volume Driver Container(Controller Plugin)を削除します。

```
$ cd ../../deploy/kubernetes
$ kubectl delete -f csi-attacher-nfsplugin.yaml
service "csi-attacher-nfsplugin" deleted
statefulset.apps "csi-attacher-nfsplugin" deleted

$ kubectl delete -f csi-attacher-rbac.yaml
serviceaccount "csi-attacher" deleted
clusterrole.rbac.authorization.k8s.io "external-attacher-runner" deleted
clusterrolebinding.rbac.authorization.k8s.io "csi-attacher-role" deleted
```
続いて、CSI Volume Driver Container(Node Plugin)を削除します。

```
$ kubectl delete -f csi-nodeplugin-nfsplugin.yaml
daemonset.apps "csi-nodeplugin-nfsplugin" deleted

$ kubectl delete -f csi-nodeplugin-rbac.yaml
serviceaccount "csi-nodeplugin" deleted
clusterrole.rbac.authorization.k8s.io "csi-nodeplugin" deleted
clusterrolebinding.rbac.authorization.k8s.io "csi-nodeplugin" deleted
```
母艦のmacOSのコンソールに移り、macOS上のNFS Serverの設定を削除します。

```
$ sudo vi /etc/exports

$ sudo cat /etc/exports
$
```
最後に、macOS上のNFS Serverのデーモンを停止し、検証に利用した/shareディレクトリを削除します。

```
$ sudo nfsd update
$ sudo showmount -e
Exports list on localhost:

$ sudo rmdir /share
$ sudo nfsd stop
```


# 感想
　Kubernetesでは、3rdパーティベンダが独自に開発が進められるようにout of treeとしてCSIが提供され始めました。これは、かつて仮想化環境の世界において、OpenStackがCinderを提供し、3rdパーティベンダがCinder Driverを開発していることに似ています。コンテナ環境向けに提供されているのがCSIとなります。違いとしては、Cinder Driverは、OpenStackに限定されていましたが、CSIは、Kubernetes, Mesos, Docker, CloudFoundryなどの幅広いコンテナ環境で利用できる点です。この点は3rdパーティベンダにとって好ましい点だと思います。
　しかし、CSIのVolumeの状態遷移にて示したように、現時点のCSIは非常にシンプルかつ基本機能しかありません。SnapshotやBackupといった高度なストレージの機能については、まだ姿形もありません。CSIで、これらの高度なストレージ機能が登場するまでは、管理者は外部ストレージの管理I/Fを利用し、SnapshotやBackupなどを利用することになります。もしくは、CSIのCreateVolume(もしくは、Node Publish Volume)が呼び出されると同時にSnapshotやBackupを自動設定してくれるようなCSI Driverを、3rdパーティベンダが開発してくれるのを待つかです。いずれにしても、CSIが登場したことで3rdパーティベンダがコンテナ向けのストレージを開発する「場」ができました。まだまだベイビーフェーズのCSIですが、3rdパーティベンダの開発が加速し徐々に機能が拡充されてくることに期待します。

# 参考情報
- [Introducing Container Storage Interface (CSI) Alpha for Kubernetes](http://blog.kubernetes.io/2018/01/introducing-container-storage-interface.html)
- [Kubernetes CSI Documentation](https://kubernetes-csi.github.io/docs/Home.html)
- [Container Storage Interface(CSI)](https://github.com/container-storage-interface/spec/blob/master/spec.md)
  - [CSI Spec](https://github.com/container-storage-interface/spec)
- [CSI Drivers](https://github.com/kubernetes-csi/drivers)