[k8sで作る機械学習環境]jupyterをkubernetes1.10で動かす ~GPU使用・NFSマウント~

##前提
kubernetes環境構築済み
yamlファイルが書ける
[[k8sで作る機械学習環境]Kubernetes1.10でGPUを使う~NVIDIAドライバーのインストールから~](https://qiita.com/monkeydaichan/items/f1ac1ee799e460863e3e)
↑を参考にGPUドライバーが使えること

しないこと
kubectlコマンドの使用方法
k8sの用語の詳細な説明

##ゴール
MLP01のIPでjupyterコンテナへブラウザでアクセスし、tensorflowのコードからGPUを使用できるか確認。

###本記事で構築するjupyter
・namespaceはdefaultではなく「mle」とする
・DockerRegistryからjupyterのイメージをpullして、ReplicaSetを作成しPodを1つ動かす
・PodへはMLP01のIPを使用してアクセス
・PodからMLP01のGPUを使うようにyamlファイルに記述をする
・podからnfsをマウントしてjupyterで記述したコードを保存

![jupyter_on_k8s.png](https://qiita-image-store.s3.amazonaws.com/0/177572/a0fe2a5b-4945-97e9-ca3a-08b62ae4f398.png)


##環境

```shell:OSのバージョン
# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04 LTS"
```

```shell:k8sのバージョン
# dpkg -l | grep kube
ii  kubeadm                                    1.10.3-00                           amd64        Kubernetes Cluster Bootstrapping Tool
ii  kubectl                                    1.10.3-00                           amd64        Kubernetes Command Line Tool
ii  kubelet                                    1.10.3-00                           amd64        Kubernetes Node Agent
ii  kubernetes-cni                             0.6.0-00                            amd64        Kubernetes CNI
```

##k8sでいろいろ作っていく！
###PersistentVolume
まずはNFSマウントをpodから行うためにk8sのPersistentVolume機能を使ってボリュームを作成します。
k8sのPersistentVolumeサブシステムは、ストレージがどのように消費されてからどのように提供されるかの詳細を抽象化するAPIを提供しているらしいです。
ちゃちゃっとyamlを作成しましょう。

```yaml:nfs_volume_for_jupyter.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: daichi
  labels:
    volume: jupyter-volume
    app: dev
  namespace: mle
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 100Gi
  nfs:
    server: 192.168.100.150
    path: "/opt/nfs/jupyter/daichi"
```
####accessModes
accessModesは3種類あります。
・ReadWriteOnce – 単一のノードでボリュームを読み書き可能にマウント
・ReadOnlyMany  – 複数のノードでボリュームを読み取り専用でマウント
・ReadWriteMany – 複数のノードでボリュームを読み書き可能にマウント
####capacity
capacityに容量を記述しないと、NFSサーバの容量を無制限に使えてしまうので制限を設定しています。

yamlを実行したらPVが作成されたか確認します。

```shell:PVの確認
$ kubectl get pv --namespace=mle
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM              STORAGECLASS   REASON    AGE
daichi     100Gi      RWX            Retain           Available                                               8m
```
容量が100GBでReadWriteManyのPVが作成されました！

###PersistentVolumeClaim
作成したnfsの100Giのpvからpodに紐付けるための領域を要求します。PVCを作ることでPodにストレージを追加することができます。

試しに、PVから10Giのストレージを要求するためにPVCを作成します。

```yaml:nfs_volumeclime_for_jupyter.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: daichi
  labels:
    volume: jupyter-volume
    app: dev
  namespace: mle
spec:
  resources:
    requests:
      storage: 10Gi
  accessModes:
    - ReadWriteMany
  selector:
    matchLabels:
      volume: jupyter-volume
```
kubectlコマンドで確認します。

```shell:PVCの確認
$ kubectl get pvc --namespace=mle daichi
NAME      STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
daichi    Bound     daichi    100Gi      RWX                           46s
```

statusがBoundになってれば成功です。
PVの確認をするとSTATUSがAvailableからBoundに変更されているはずです。
これでPodからアクセスする準備ができました。

```shell:PVの確認
$ kubectl get pv --namespace=mle daichi
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM              STORAGECLASS   REASON    AGE
daichi     100Gi      RWX            Retain           Bound     mle/daichi                                  5s
```

###jupyterコンテナの作成
jupyterコンテナを作成してGPU割当、PVCのマウントをしましょう
####ReplicaSet
ReplicaSetは、指定された数のPodがいつでも実行されていることを保証します。
[k8sの公式ドキュメント](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#when-to-use-a-replicaset)ではDeploymentを使ってReplicaSetを管理すべし、と書いてありますがローリングアップデートをする必要はjupyterでは無いのでReplicaSetを使用します。
今回の構成ではDeploymentではなくてReplicaSetを使用します。

```yaml:replicaset_for_jupyter
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
  name: jupyter
  labels:
    app: jupyter
    pj: dev
  namespace: mle
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jupyter
  template:
    metadata:
      labels:
        app: jupyter
        pj: dev
      namespace: mle
    spec:
####コンテナのイメージを指定###########
      containers:
      - name: notebooks
        image: 192.168.100.150:5000/daichi/notebooks:1.2
        resources:
####コンテナが要求するリソース###########
          requests:
            cpu: "4000m"
            memory: "8Gi"
####ここでGPUを要求###################
            nvidia.com/gpu: 1
####コンテナに割り当てるリソース#########
          limits:
            cpu: "8000m"
            memory: "16Gi"
            nvidia.com/gpu: 1
####jupyterが動くポートを指定###########
        ports:
        - containerPort: 9999
        volumeMounts:
          - name: daichi
####マウントしたいコンテナ内のpathを記載###
            mountPath: "/notebooks"
      volumes:
        - name: daichi
####PVCを指定#########################
          persistentVolumeClaim:
            claimName: daichi
####コンテナのDNSサーバの向き先を指定#####
      dnsConfig:
        nameservers:
          - 8.8.8.8
```
yaml内にコメントアウトで説明書きました。
注意事項はDockerRegistryからイメージをpullする場合、すべてのNodeからdocker login 192.168.100.150:5000を事前に実行しておく必要があります。
でないと、イメージのpullが失敗してしまいます。

ReplicaSetができたか確認します。

```shell:ReplicaSetの確認
$ kubectl get replicaset --namespace=mle jupyter
NAME      DESIRED   CURRENT   READY     AGE
jupyter   1         1         1         5m
```

Podができているかの確認もしときます。

```shell:Podの確認
$ kubectl get pod --namespace=mle jupyter-xvmcj
NAME            READY     STATUS    RESTARTS   AGE
jupyter-xvmcj   1/1       Running   0          6m
```

####Service
最後に作ったjupyterのPodにアクセスするためにServiceを作成します。

```yaml:service_for_jupyter.yaml
kind: Service
apiVersion: v1
metadata:
  name: jupyter
  labels:
    app: jupyter
    pj: dev
  namespace: mle
spec:
  selector:
    app: jupyter
  ports:
  - name: port-jupyter
####コンテナ内のport###################
    port: 9999
    protocol: TCP
    targetPort: 9999
####ブラウザからアクセスするときのport#####
    nodePort: 30888
  type: "NodePort"
```

service_for_jupyter.yamlを実行するとServiceが作成されます。
ブラウザからアクセスするさいのポートは**30888**になります。

```shell:Serviceの確認
$ kubectl get svc --namespace=mle jupyter
NAME      TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
jupyter   NodePort   10.100.202.239   <none>        9999:30888/TCP   6m
```

##jupyterにアクセス
NodeのIPでアクセスするためにどこのNodeで動いてるか確認します。

```shell:Podの詳細情報
$ kubectl describe pod --namespace=mle jupyter-xvmcj
Name:           jupyter-xvmcj
Namespace:      mle
Node:           mlp03/192.168.200.153
Start Time:     Thu, 07 Jun 2018 17:15:36 +0900
Labels:         app=jupyter
                pj=dev
Annotations:    <none>
Status:         Running
IP:             10.244.2.51
Controlled By:  ReplicaSet/jupyter
Containers:
  notebooks:
    Container ID:   docker://f4c0e6c321bdb47b6ad39eb88f41984d9d1c88698fab7b86f47f50602e6978b5
    Image:          192.168.100.150:5000/daichi/notebooks:1.2
    Image ID:       docker-pullable://192.168.100.150:5000/daichi/notebooks@sha256:fb2199cce57e7bcfead825860f42c3e668a641ab91293ee25e3e098be7095cda
    Port:           9999/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Thu, 07 Jun 2018 17:15:38 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:             8
      memory:          16Gi
      nvidia.com/gpu:  1
    Requests:
      cpu:             4
      memory:          8Gi
      nvidia.com/gpu:  1
    Environment:       <none>
    Mounts:
      /notebooks from daichi (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-fhmb8 (ro)
Conditions:
  Type           Status
  Initialized    True
  Ready          True
  PodScheduled   True
Volumes:
  daichi:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  daichi
    ReadOnly:   false
  default-token-fhmb8:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-fhmb8
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason                 Age   From               Message
  ----    ------                 ----  ----               -------
  Normal  Scheduled              9m    default-scheduler  Successfully assigned jupyter-xvmcj to mlp03
  Normal  SuccessfulMountVolume  9m    kubelet, mlp03     MountVolume.SetUp succeeded for volume "default-token-fhmb8"
  Normal  SuccessfulMountVolume  9m    kubelet, mlp03     MountVolume.SetUp succeeded for volume "daichi"
  Normal  Pulled                 9m    kubelet, mlp03     Container image "192.168.100.150:5000/daichi/notebooks:1.2" already present on machine
  Normal  Created                9m    kubelet, mlp03     Created container
  Normal  Started                9m    kubelet, mlp03     Started container
```
MLP03で動いてますね。
http://192.168.200.153:30888/
にブラウザからアクセスするとjupyterの画面が見えるはずです。
![スクリーンショット 2018-06-07 17.28.37.png](https://qiita-image-store.s3.amazonaws.com/0/177572/8853ab54-7376-dd12-1cec-ba003e82ab12.png)

tensorflowのコードを実行してGPUをちゃんと使えているか確認してみます。

```python:GPUの確認
from tensorflow.python.client import device_lib
device_lib.list_local_devices()
```
![スクリーンショット 2018-06-07 17.30.23.png](https://qiita-image-store.s3.amazonaws.com/0/177572/a267ab11-52d1-3b37-b1f2-c8e6ce36accf.png)

ちゃんとGPU使えてますね！！！

やりたいこと
Podを動かすNodeは指定できるので、その方法を近々追記します。GPUの指定はできるのかな？？
mysqlをk8s上で動かしたいですね〜。
Devlopmentとかingressも使ってみたいです。