コンテナ初心者がKubernetesによるNFSボリュームマウントができるようになるまで



この記事は[NSSOL Advent Calendar](https://qiita.com/advent-calendar/2018/nssol)の11日目の記事です。

## 概要

弊社ではDeep Learning開発を効率的化する「[KAMONOHASHI](https://kamonohashi.ai/)」というプラットフォームを開発しています。[^1]
私はその開発に携わっています。
「KAMONOHASHI」は内部でKubernetes(k8s)を使用しています。

「KAMONOHASHI」の追加機能を検討していた際に、Kubernetes(k8s)でNFSボリュームマウントする必要のある機能の改修が持ち上がりました。

私はそれまでUXがメイン担当だったので「Docker何それ???」、「Ku、Kube...読めない」な状態だったのですが、「Docker,k8s触りたいです！」などと軽率に言ってみたところ先輩方から「せっかくだからやってみなよ」というありがたいコメントをいただき、機能改修にチャレンジしてみることになりました。

そんなコンテナ初心者がk8sによるNFSボリュームマウントに挑戦した際に、勉強していった内容や、`先輩たち「k8sちょっと勉強してyamlを書き換えるだけだよー」→全然違った()というお話を書いています。

## 対象読者

Docker/Kubernetes(k8s)これからがんばるぞ！でもコンテナ詳しくない...という程度。
結構初歩的なことから書いているので、Docker/Kubernetes ちょっと知りたいと思っている方におすすめです。
すでにコンテナチョットデキル、な方は[ozota先輩のAdvent Calendar](https://qiita.com/advent-calendar/2018/botti)を読むのをおすすめします。[^2]

## 今回やったこと
[KAMONOHASHI](https://kamonohashi.ai/)ではDeep Learningの学習結果をNFSに保存しています。これはNFS共有を通じて、
Deep Learningの学習途中でも学習の進捗が分かるようにするためです。
AIの精度向上度合いをグラフ表示したりしています。

これまでは学習結果の書き出しのみにNFSを使用していたのですが、
以前学習した結果を次の学習の入力にする機能が欲しいという要望を受け、
新規学習開始時にNFSに保存されている過去の学習結果もマウントするように改修しました。
![今回やったこと.png](https://qiita-image-store.s3.amazonaws.com/0/258705/bd29e499-56bc-e044-64fc-e70cd5b86990.png)



## 勉強・実装した内容

##### 1. [Dockerの概要を掴む](#1-dockerの概要を掴む)
##### 2. [Dockerでボリュームマウントを試す](#2-dockerでボリュームマウントを試す)
##### 3. [k8sの概要を掴む](#3-k8sの概要を掴む)
##### 4. [k8sのボリュームマウントを試す](#4-k8sのボリュームマウントを試す)
##### 5. [実装](#5-実装)
##### 6. [最後にどんでん返しが起こる](#6-最後にどんでん返しが起こる)

## 1.Dockerの概要を掴む

1. Qiitaの[Docker解説記事](https://qiita.com/pottava/items/7086f9d63487aa3d7d08)でDocker概要をつかむ
    - 記事を読んでコンテナとはプロセスを隔離して起動する技術であることをなんとなく理解しました。　　　
    - Docker登場以前にあった課題とDockerの機能によって解決する課題を下記にまとめました。
    
    |従来技術|何が問題だったか|Dockerが解決する内容|
 |:--|:--|:--|
 |構成管理|同じ定義書を利用しても、隣の人とまったく同じ環境になる保証がない|gitのようにcommit, push, pullを使った作業の履歴管理ができる。構成管理ツールを使いDockerイメージを作り、レジストリを介して共有することにより解決| 
|構成管理|ひとつのプロジェクトで、複数バージョンのソフトウェアを同時に使うのは困難|OSディストリビューションごとアプリが分離できるので同時に扱えるように　| 
|VM|VMスナップショットからは作業履歴を追いにくい|イメージに対して編集した内容は、$ docker historyで確認できる| 
|VM|サーバは数分で起動するが、数分だと時間がかかりすぎ|起動も停止も一瞬　|
|VM|ポータビリティが下がるベンダーロックインの心配|Windowsを含めた大手OS、クラウド各社がサポートを表明|


1. [Dockerをインストール](http://tracpath.com/works/devops/2017-docker-install/)
1. [Docker公式チュートリアル](https://docs.docker.com/get-started/)を流しました。[解説記事](https://qiita.com/Tocyuki/items/420c3afa25bd28e58159)も必要に応じて参考にしました。

## 2.Dockerでボリュームマウントを試す

- Dockerにおけるマウントとは[^3]

コンテナの外にあるデータを、コンテナの中で利用できる状態にすること。
あるいはコンテナの中にあるデータを、コンテナの外で利用できる状態にすること。

- ボリュームマウントとは

コンテナの内/外のファイルやディレクトリをコンテナの外/内で利用できるようにする機能のこと。

- [Qiitaの記事](https://qiita.com/namutaka/items/f6a574f75f0997a1bb1d)を参考にvolumeマウントについて試しました。

```docker:Dockerでボリュームマウントを試す
docker run -v /host/path:/container/path some_image
// ホストの /host/path を、コンテナの /container/path にマウント
```

KAMONOHASHIではファイルの随時書き込みを行うNFSの機能と、ブラウザアクセスを行う機能を併せ持つ[minio](https://www.minio.io/)のnas gateway機能を利用しています。これにより、学習中にリアルタイムにログを確認したり、データをブラウザからダウンロードしたりすることを可能にしています。

ということでminioのイメージを使用しコンテナを起動してみました。
起動する際に、ホストの/mnt/dataをコンテナの/dataにマウントしました。

```docker:ホストの/mnt/dataをコンテナの/dataにマウント
docker run -it -p 9000:9000 --name minio  \
-e "MINIO_ACCESS_KEY=****" \
-e "MINIO_SECRET_KEY=****" \
-v /mnt/data:/data \
minio/minio server /data
```

```docker:マウント情報の確認
docker inspect --format='{{.Mounts}}' コンテナID 
[{bind  /mnt/data /data   true rprivate} ]
```
ホストの```/mnt/data```がコンテナの```/data```にマウントされていることが確認できました。
 
## 3.k8sの概要を掴む

- k8sを使うと何が出来るのか[^4]

前段ではDockerを利用することでホスト上にコンテナを立ち上げることができました。
しかしKAMONOHASHIでコンテナを利用しようとすると下記のようなことを考える必要がありました。

   - 複数のDockerホストの管理
   - コンテナのスケジューリング
   - コンテナの死活監視
   - ログの管理
   - Infrastructure as Code

コンテナオーケストレーションエンジンであるKubernetes(k8s)を利用することで、これらの課題を解決することができます。k8sではYAMLでマニフェストを書くことで、実行するコンテナなどを宣言的なコードにより管理しています。

ちょうど社内でk8s勉強会が実施されていたため、[Katacoda](https://www.katacoda.com/courses/kubernetes/playground)でハンズオンしつつコンテナ仮想化技術からkubernetesの基礎知識を学びました。Katacodaは、コンテナ技術に強い無料の学習サイトで、ブラウザから簡単に試すことができるため、とりあえず触ってみたい人にはおすすめだそうです。[^5]

実際ハンズオンで触ってみて、k8sの環境構築をせずにいきなりk8sのコマンドを叩けるのが、環境構築で躓きそうな私にとってはありがたいポイントでした。(ちなみにk8sの環境構築についてはozota先輩が[12/1の記事](https://qiita.com/ozota/items/027befd3fa77a826ea62)に書いています。)



:pencil: **先輩から一言**
>Kubernetesは一般向けには上記のメリットがあるのですが、Deep LearningプラットフォームであるKAMONOHASHIにとっての一番のメリットは、GPUのスケジューリング機能です。詳しく知りたい人は「nvidia-docker」や「device plugin」で検索してください。この記事の内容からはそれてしまうので解説はしません

## 4.k8sのボリュームマウントを試す

- k8sでのvolumeの書き方

k8sでコンテナ（Pod）を作成する際の、データの格納場所（Volume）を指定する方法を調査しました。
コンテナ(Pod)が死んでしまうと、データが消滅してしまうので、Volumeに格納しておく事で、状態を維持できます。
タイプは何種類もありますがここではhostpath,nfsの書き方を紹介します。

 - 1. [hostpath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath)
k8sノード上の領域をコンテナにマッピングするプラグイン。

```yaml:hostpath例
    apiVersion: v1
    kind: Pod
    metadata:
    name: test-pd
    spec:
    containers:
    - image: k8s.gcr.io/test-webserver
        name: test-container
        volumeMounts:
        - mountPath: /test-pd
        name: test-volume
    volumes:
    - name: test-volume
        hostPath:
        # directory location on host
        path: /data
        # this field is optional
        type: Directory
```
![グループ化 380.png](https://qiita-image-store.s3.amazonaws.com/0/258705/8c8f7c9d-93d6-0d1f-a256-9879e0a041f5.png)



 - 2. [nfs](https://kubernetes.io/docs/concepts/storage/volumes/#nfs)
PodがTerminateされてもnfsボリュームの内容は保存されます。

```yaml:nfs例
 apiVersion: v1
 kind: Pod
 metadata:
   name: nfs-web
 spec:
   containers:
 - name: web
   image: nginx
   ports:
     - name: web
       containerPort: 80
   volumeMounts:
       - name: nfs
         mountPath: "/usr/share/nginx/html"
   volumes:
     - name: nfs
       nfs:
         server: nfs-server.default.kube.local
         # directory location on nfs
         path: "/" 
```    
![グループ化 377.png](https://qiita-image-store.s3.amazonaws.com/0/258705/09ef9291-a436-4327-ebad-d9aed4e94189.png)

:pencil: **先輩から一言**
>Kubernetesに詳しい人はPV・PVCについて気になる方もいるかもしれません。 KAMONOHASHIでは、記述が簡単、内部コンポーネントが少ない（PV回りのバグを避けられる期待）、容量の制限は不要（割り切り）等の考慮から上記の方法を採用しています。


## 5.実装
4.まででk8sによるNFSマウントの記述方法が分かりました。 いよいよKAMONOHASHIの実装です。

KAMONOHASHIはWebアプリであり、ユーザーが学習を開始するリクエストを投げた際に内部でKubernetesのYAMLを組み立てています。

また、KAMONOHASHIでは学習実施ごとにNFS上のディレクトリ（nfs-server:/training/trainingId/）を作成し、 コンテナ内の/kqi/outputにマウントしています。ユーザーにはそこに学習結果を書き出してもらうという仕様です。


```yaml:これまで内部で組み立てていた学習結果のNFSマウントをするYAML
# 学習2の結果(/kqi/output)をnfs(nfs-server:/training/2/)にマウント
 apiVersion: v1
 kind: Pod
 metadata:
   name: training2
 spec:
   containers:
 - name: training2
   image: tensorflow
   ports:
     - containerPort: 80
   volumeMounts:
       - name: nfs
        # k8sコンテナのvolume
         mountPath: "/kqi/output"
   volumes:
     - name: nfs
       nfs:
         server: nfs-server
         # directory location on nfs
         path: "/training/2/" 
```   

ついに、以前の学習結果のマウントを指定した場合の学習結果のNFSマウントを試します。

```yaml:以前の学習結果のマウントを指定した場合に組み立てるYAML
# 過去の学習1の結果をコンテナの/kqi/parentにマウントして学習2を実行する
 apiVersion: v1
 kind: Pod
 metadata:
   name: training2
 spec:
   containers:
 - name: training2
   image: tensorflow
   ports:
     - containerPort: 80
   volumeMounts:
       - name: parent
        # k8sコンテナのvolume
         mountPath: "/kqi/parent"
   volumes:
     - name: parent
       nfs:
         server: nfs-server
         # directory location on nfs
         path: "/training/1/" 
```   

完成です！
<img src="https://qiita-image-store.s3.amazonaws.com/0/258705/0ee03337-1f3e-fea7-58ca-132696646f60.png" width=50%/>


### ...と思いきや、<font color="Red">完全に間違っていました。</font>
![解説.png](https://qiita-image-store.s3.amazonaws.com/0/258705/9bcf997e-af02-982e-0ef0-455ee3c0b248.png)





以前の学習結果(/kqi/parent/)を読み込んで新しい学習を実行する際に、新しい学習の結果を保存するディレクトリ(/kqi/output/)を用意するのを忘れていました。ここを修正すればOKのはず。。。


## 6.最後にどんでん返しが起こる
なんとKAMONOHASHIの仕様上、volumeMountsのmountPathを条件によって変更する記述は実はyamlファイルを書かなくてもできるようになっていました。~~ここまでyamlを必死に書いていた自分は唖然としました~~

:pencil: **先輩から一言**
>後でNFSマウントはどうせ増えるだろうとソースコードに拡張性を持たせてたのですが、実装したのがずいぶん昔で完全に忘れていました。 ごめん！:sweat_smile:

## おわりに

今回はk8sによるNFSボリュームマウントに挑戦した話を書きました。
冒頭に、
```先輩たち「k8sちょっと勉強してyamlを書き換えるだけだよー」→全然違った()```
と書いた通り、コンテナ初心者には結構難しかったです。
特に2個のボリュームをマウントしている例が意外とネット上では見つからず初心者にとっての混乱ポイントでした。
僭越ながら、これからdocker,k8sを始める方の参考になれば幸いです。

ちなみに、今回苦戦して開発した機能で、KAMONOHASHIユーザの方々の[kaggle6位](https://www.kaggle.com/c/quickdraw-doodle-recognition/leaderboard)に貢献できたようで、ユーザが喜ぶ機能を作れてよかったなあと思っています。[^6]

[^2]: 同じグループの先輩。インドに行っている間に弊社のAdvent Calendarが埋まってしまったため、個人で毎日記事を執筆しています。
[^1]: KAMONOHASHIは新日鉄住金ソリューションズ株式会社の登録商標です。 
[^3]: [さわって理解するDocker入門](https://www.ogis-ri.co.jp/otc/hiroba/technical/docker/part7.html)
[^4]: 大変分かりやすい解説記事でした。[今こそはじめようKubernetes入門](https://thinkit.co.jp/series/7342)
[^5]: 今回執筆時に見つけたブログです。[コンテナ技術の学習するなら圧倒的にKatacodaがおすすめ](https://budougumi0617.github.io/2018/06/10/katacoda-introduction/)
[^6]: 詳細はもうすぐ広報がでると期待しています。