Edited at

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では、Kubernetesの Node, MasterのRoot FileSystem にアクセスが必要であり、デプロイが煩雑でした。

 Kubernetesでは、CSIをサポートすることで、Kubernetesのソースに組み込まず3rdパーティベンダが独自に実装できる("out-of-tree")にて提供することが可能となりました。さらに、Kubernetesでは以前より備えているStorageClass, PersistentVolume, PersistentVlumeClaimを引き続きCSIでも使うことで、コンテナをデプロイしようとするユーザから見て、v1.8以前と同様の操作でCSI経由で外部ストレージを利用することが可能となります。

 このCSIは,Kubernetes, Mesos, Docker, CloudFoundry など幅広くコンテナ環境で利用できるようにすべく、Kubernetesとは独立し、Container Storage Interface コミュニティにて仕様が策定されています。


KubernetesにおけるCSIのアーキテクチャ

KubernetesにおけるCSIのアーキテクチャ図を示します。

csi-arch.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

まず、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

- 外部ストレージ: NFS Server(Mac macOS(10.12.6)のNFS Serverを利用)

※ kubeadmを使ったKubernetes v1.10の構築手順については割愛します。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を利用します。

$ 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が用意している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パーティベンダの開発が加速し徐々に機能が拡充されてくることに期待します。


参考情報