Local Volumeを使ったKubeflowの動作検証

#はじめに
　今回の検証では、Kubernetes上でML環境を提供する[Kubeflow](https://www.kubeflow.org)を試します。Kubeflowは、シングルノードのKubernetesの実験環境であるminikubeでも実行できます。しかし、大規模な計算を行うMLにおいて、シングルノードの計算リソースでは実用に耐えられません。そのため、マルチノードのKubernetesクラスタ上にKubeflowをセットアップしたい人も多いかと思います。
　しかし、セットアップに際し、ひとつ問題があります。それが永続ストレージです。Kubeflowをセットアップするためには、データを保存するための永続ストレージが必要となります。minikubeでは、minikubeが提供するprovisionerによって、minikubeのVMを利用し永続ストレージを提供しています。GKEなどのパブリッククラウドのサービスを使っている場合も、比較的容易に永続ストレージを準備することができます。しかし、自宅や小規模なオフィスなどでKubernetesを運用している場合は、なかなか準備できない人も多いかと思います。さらには、Kubeflowで必要とされている永続ストレージはDBなどのバックエンドで利用することから、ファイルストレージ(NAS)は非推奨であることからブロックストレージを準備しないといけません。
　そこで、今回は、自宅や小規模なオフィスでも容易に準備できるブロックストレージとして[Kubernetes: Local Volumeの検証](https://qiita.com/ysakashita/items/67a452e76260b1211920)で紹介したLocal Volumeを使ってKubeflowを使いKubeflowの動作検証を行います。

#検証環境
以下のKubernetes環境を使います。

- Kubernetes v1.11.1
  - Master x 1, Worker x 2
- Kubeflow v0.3

#動作検証
## ksnonnet のインストール
Kubeflow 0.3では、ksonnet 0.11.0 or laterが必要になります。
そのため、brewを使いksonnetをインストールします。

```console
$ brew install ksonnet/tap/ks
...

$ ks version
ksonnet version: 0.13.0
jsonnet version: v0.11.2
client-go version: kubernetes-1.10.4
```

##Kubeflowのダウンロード
GitHubにあるKubeflowのセットアップに必要なファイル一式をダウンロードします。

```console
$ mkdir kubeflow
$ cd kubeflow/
$ export KUBEFLOW_TAG=v0.3.0
$ curl https://raw.githubusercontent.com/kubeflow/kubeflow/${KUBEFLOW_TAG}/scripts/download.sh | bash
...
```
ダウンロードが完了すると`kubeflow`と`script`ディレクトリが作成され、その配下にセットアップで利用するスクリプトが配置されます。

##Kubeflowのセットアップ
はじめに、セットアップ用の環境変数の設定スクリプトを生成します。

```console
$ scripts/kfctl.sh init kfapp --platform none
...

$ cd kfapp/
```
次に、環境変数の設定スクリプトのあるディレクトリにて、ksonnet appを生成します。

```console
$ ../scripts/kfctl.sh generate k8s
...
```
続いて、Kubeflowをデプロイする先のNamespace(`kubeflow`)をKubernetes上に作成します。

```console
$ kubectl create ns kubeflow
```
Kubeflowをデプロイします。

```console
$ ../scripts/kfctl.sh apply k8s
...
```
デプロイが完了後、KubernetesにデプロイされたPodを確認します。

```console
$ kubectl get pod
NAME                                        READY   STATUS    RESTARTS   AGE
ambassador-868d5dbdc4-6psgx                 3/3     Running   0          22m
ambassador-868d5dbdc4-rxz2s                 3/3     Running   0          22m
ambassador-868d5dbdc4-wpw67                 3/3     Running   0          22m
argo-ui-84464bd59c-9gwsn                    1/1     Running   0          21m
centraldashboard-c76877875-x4pgb            1/1     Running   0          22m
modeldb-backend-58969447f6-hszg9            1/1     Running   0          21m
modeldb-db-57b855f5b7-lc47p                 1/1     Running   0          21m
modeldb-frontend-769d5bdd66-77964           1/1     Running   0          21m
spartakus-volunteer-5974ff4c8c-8qmfr        1/1     Running   0          20m
studyjob-controller-56588dc6f9-l4rqc        1/1     Running   0          21m
tf-hub-0                                    1/1     Running   0          22m
tf-job-dashboard-7777b6bf-mlk2g             1/1     Running   0          22m
tf-job-operator-v1alpha2-5c5b4dcfdf-2knwd   1/1     Running   0          22m
vizier-core-5584ccbd8-wt9zm                 0/1     Error     6          21m
vizier-db-547967c899-sgmmz                  0/1     Pending   0          21m
vizier-suggestion-grid-8547dbb55b-4qpwp     1/1     Running   0          21m
vizier-suggestion-random-d7c5cd68b-fg7sl    1/1     Running   0          21m
workflow-controller-5c95f95f58-cpbh5        1/1     Running   0          21m
```
すると、残念ながらvizierの起動に失敗しています。
失敗している原因は、vizer-db(実体はMySQL)にPersistentVolumeが割り当てられていないためです。そのため、vizier-dbがPendingとなりvizier-coreの起動に失敗しています。
kubectl describeコマンドで確認することで原因がわかります。
なお、PersistentVolumeがDynamic Provisioningできる環境であれば、本問題は発生しないかもしれません。

```console
$ kubectl describe pod vizier-db-547967c899-sgmmz
...
Events:
  Type     Reason            Age                    From               Message
  ----     ------            ----                   ----               -------
  Warning  FailedScheduling  3m46s (x363 over 23m)  default-scheduler  pod has unbound PersistentVolumeClaims (repeated 3 times)

$ kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
vizier-db   Pending                                                     24m

$ kubectl get pv
No resources found.
```
そこで、[Kubernetes: Local Volumeの検証](https://qiita.com/ysakashita/items/67a452e76260b1211920)を参考にし、Local Volumeを割り当てるようにします。
作成したPersistent Volumeは以下です。

```console
$ kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    REASON   AGE
local-pv   10Gi       RWO            Retain           Available           local-storage            17s
```
以上で、KubeflowがKubernetes上にデプロイされます。
vizer-dbが利用するPersistentVolumeClaim(`vizier-db`)が要求しているサイズが10Gbyteのため、Local VolumeのPersistentVolume(`local-pv`)も10GByteとしています。
次に、PersistentVolumeClaim(`vizier-db`)と、PersistentVolume(`local-pv`)をBoundさせます。 
まず、PersistentVolumeClaim(`vizier-db`)をYAMLフォーマットでManifestファイルに保存します。

```console
$ kubectl get pvc vizier-db -o yaml > vizier-db.yaml
```
続いて、保存したManifestファイルを編集します。
編集箇所を以下に示します。
特に必要なのは、.spec.storageClassNameにLocal Volume用のStorageClassである`local-storage`を指定することです。

```diff
--- vizier-db.yaml.org  2018-10-17 17:45:09.000000000 +0900
+++ vizier-db.yaml  2018-10-17 17:45:41.000000000 +0900
@@ -3,7 +3,6 @@
 metadata:
   annotations:
     ksonnet.io/managed: '{"pristine":"H4sIAAAAAAAA/0yOsU7tQAxE+/cZU+cBt01LQYVAFJcCUTibAVlJ1svaAYmr/Xe0KRDd6Nhz7Auk6JnV1TJGfJ4wYNE8Y8Rjpx7McbZ133i7im4YsDFklhCMF6wycfWepJTe129lxYBkW7HMHBgxT13qljPjSu3672yR0AltQJaNv4L/R6UjL5I6X/aJb6t99VUvTMfJlOh+bzMd4wueKPNz1eBDTsTrgEq3vSYe/1V+7PQ4sodVee/a082dorXW/v0AAAD//wEAAP//j1cT1gkBAAA="}'
-  creationTimestamp: 2018-10-17T07:51:18Z
   finalizers:
   - kubernetes.io/pvc-protection
   labels:
@@ -14,13 +13,11 @@
   name: vizier-db
   namespace: kubeflow
   resourceVersion: "4672936"
-  selfLink: /api/v1/namespaces/kubeflow/persistentvolumeclaims/vizier-db
   uid: 6eb34226-d1e1-11e8-a0a0-080027571559
 spec:
+  storageClassName: local-storage
   accessModes:
   - ReadWriteOnce
   resources:
     requests:
       storage: 10Gi
-status:
-  phase: Pending
```
編集が完了後、Manifestファイルを使いデプロイします。

```console
$ kubectl delete pvc vizier-db
persistentvolumeclaim "vizier-db" deleted

$ kubectl create -f vizier-db.yaml
persistentvolumeclaim/vizier-db created
```
PersistentVolumeClimのデプロイが完了すると、vizier-coreとvizier-dbが起動します。

```console
$ kubectl get pod
...
vizier-core-5584ccbd8-wt9zm                 1/1     Running   12         1h
vizier-db-547967c899-sgmmz                  1/1     Running   0          1h
...
```

これでKubeflowのPodが全て起動しました。

##動作確認

###port-forwadingの設定
次のコマンドでreverse proxyを提供するambassadorのPodにポートをフォワードします。

```console
$ export NAMESPACE=kubeflow
$ kubectl port-forward -n ${NAMESPACE}  `kubectl get pods -n ${NAMESPACE} --selector=service=ambassador -o jsonpath='{.items[0].metadata.name}'` 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
```

Webブラウザで`http://localhost:8080/`にアクセスすると以下のようなKubeflow navigation UIにアクセスできます。

![スクリーンショット 2018-10-17 18.16.27.png](https://qiita-image-store.s3.amazonaws.com/0/229880/8fe2a383-4e7f-d19d-015d-c326704f65d9.png)

###Jupyter Notebookへのアクセス

Kubeflow navigation UIから`JUPYTERHUB`を選択します。
すると、ログイン画面が起動するため、好きなUsername, Passwordを入力し`Sign in `をクリックします。

![スクリーンショット 2018-10-17 18.13.49.png](https://qiita-image-store.s3.amazonaws.com/0/229880/8d16b4a7-32cb-751e-3b99-76b15baadd0b.png)

次にSpawnerの選択画面が出てます。

![スクリーンショット 2018-10-17 18.14.01.png](https://qiita-image-store.s3.amazonaws.com/0/229880/5701f10f-9cd2-9449-9c00-7e19b8daaeea.png)

Imageに、tensorflowのイメージを指定します。
GPUを使う人は、イメージ名に`gpu`が入ったものを選択し、CPUのみ使う人は`cpu`が入ったものを選択します。

![スクリーンショット 2018-10-17 18.38.28.png](https://qiita-image-store.s3.amazonaws.com/0/229880/f32f2f5e-2a86-d840-f926-06f742ef78ca.png)

Imageを選択後、`Spawn`を選択します。
すると、Jupyter Notebookの環境作成が始まります。

![スクリーンショット 2018-10-17 18.14.43.png](https://qiita-image-store.s3.amazonaws.com/0/229880/d3833bd0-fd42-1a61-6e2b-e2d9c5e67b98.png)

ここで、数分待っても終わらないようであれば、Jupyter NotebookのPodの状態を確認してください。kubectl getコマンドで確認すると、jupyter-<Username>のPod(本検証ではjupyter-ysakashita)が生成され、`Pending`になっています。 
これは、vizier-dbと同じく、jupyter-<Username>のPodが利用するPersistent Volumeが作成出来ていないからです。

```console
$ kubectl get pod
NAME                                        READY   STATUS              RESTARTS   AGE
...
jupyter-ysakashita                          0/1     Pending
   0          2m
...

$ kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
...
claim-ysakashita   Pending                                                     2m

```
そこで、[Kubernetes: Local Volumeの検証](https://qiita.com/ysakashita/items/67a452e76260b1211920)を参考にし、Local Volumeを割り当てるようにします。
作成したPersistent Volumeは以下です。

```console
$ kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    REASON   AGE
...
local-pv2   10Gi       RWO            Retain           Available           local-storage            18s
```
次に、PersistentVolumeClaim(`claim-ysakashita`)と、PersistentVolume(`local-pv2`)をBoundさせます。 
まず、PersistentVolumeClaim(`claim-ysakashita`)をYAMLフォーマットでManifestファイルに保存します。

```console
$ kubectl get pvc claim-ysakashita -o yaml >claim-ysakashita.yaml
```
続いて、保存したManifestファイルを編集します。
編集箇所を以下に示します。

```diff
$ diff -u claim-ysakashita.yaml.org claim-ysakashita.yaml
--- claim-ysakashita.yaml.org 2018-10-17 18:25:18.000000000 +0900
+++ claim-ysakashita.yaml 2018-10-17 18:25:59.000000000 +0900
@@ -3,7 +3,6 @@
 metadata:
   annotations:
     hub.jupyter.org/username: ysakashita
-  creationTimestamp: 2018-10-17T09:14:11Z
   finalizers:
   - kubernetes.io/pvc-protection
   labels:
@@ -12,9 +11,9 @@
   name: claim-ysakashita
   namespace: kubeflow
   resourceVersion: "4681970"
-  selfLink: /api/v1/namespaces/kubeflow/persistentvolumeclaims/claim-ysakashita
   uid: 026fd018-d1ed-11e8-a0a0-080027571559
 spec:
+  storageClassName: local-storage
   accessModes:
   - ReadWriteOnce
   resources:
```

編集が完了後、Manifestファイルを使いデプロイします。

```console
$ kubectl delete pvc claim-ysakashita
persistentvolumeclaim "claim-ysakashita" deleted

$ kubectl create -f claim-ysakashita.yaml
persistentvolumeclaim/claim-ysakashita created
```
PersistentVolumeClimのデプロイが完了すると、jupyter-ysakashitaのPodが起動します。

```console
$ kubectl get pod
NAME                                        READY   STATUS    RESTARTS   AGE
...
jupyter-ysakashita                          1/1     Running   0          19m
...
```
Podが起動すると、以下のようにJupyter Notebookが立ち上がります。

![スクリーンショット 2018-10-17 18.33.21.png](https://qiita-image-store.s3.amazonaws.com/0/229880/3f0e2488-a80e-a17e-41dc-86f12b9f18fa.png)

Jupyter Notebookで`new`を選択し、新しいソースコードを作成し開発を開始します。

![スクリーンショット 2018-10-17 19.02.26.png](https://qiita-image-store.s3.amazonaws.com/0/229880/440497cb-0411-a75f-9cda-7d2e6aefe5f1.png)

以上で、KuberflowのセットアップとJupyter Notebookの起動まで動作検証を実施しました。

#おわりに
　今回は、Local Volumeを使ったKubeflowのセットアップと動作検証を行いました。
これまで、永続ストレージが準備できずminikubeで仕方なくKubeflowを使っていた人もいたかと思います。minikubeだと、どうしてもCPUやメモリなどのリソースが不足しがちになります。
今回の検証で単にLocal Volumeを使うというだけでなく、minikubeから卒業しCPUやメモリに加えGPUなどのリソースが潤沢に使えるKubernetesクラスタ環境へ移行出来るのではないでしょうか。
　また、今回利用したLocal Volumeは、自宅や小規模オフィスのようにあまりインフラにお金をかけられない環境において、手軽に利用できるブロックストレージです。しかし、2018/10時点での最新のKubernetes 1.12.1でもLocal Volumeは、未だDynamic Provisioningが正式にサポートされていません。KubeflowのようにDynamic Provisioningを前提としている場合、Local Volumeはまだまだ扱いにくいのが現状です。今回実施したように一つ一つ確かめながらLocal Volumeを割り当てていく必要があります。そのため、非常に手間がかかってしまうのが残念です。お金をかけるか手間をかけるかの天秤ですね。もし、Local VolumeのDynamic Provisioningの正式サポートが待てないという人は[Kubernetes incubator](https://github.com/kubernetes-incubator/external-storage)のDynamic Provisioningを利用する方法もあります。私も機会があれば試してみたいと思います。

#参考情報
- [Kubeflow](https://www.kubeflow.org)
- [GitHub kubeflow/kubeflow](https://github.com/kubeflow/kubeflow)