AI開発基盤KAMONOHASHIで任意のコンテナが使える仕組み

OSSのAI開発基盤[KAMONOHASHI](https://github.com/KAMONOHASHI/kamonohashi)の内部でのKubernetesの使い方の解説記事です。

# 解説するKAMONOHASHIの機能
* KAMONOHASHIではユーザーが指定した任意のコンテナ上で機械学習を実行できます。
    ![](https://kamonohashi.ai/assets/images/figure7-2.png)
* ただコンテナ上でユーザー指定のコマンド実行すればよい、というわけではなく、機械学習実行用に次のような準備・後処理をする必要があります。
  * 指定Gitからソースコードを取得してコンテナ内の特定ディレクトリ(`/kqi/git/`)に配置
  * KAMONOHASHIのAPIをたたいて学習ステータス通知（現状は完了・失敗報告のみ）[^1]
  * ...etc
         
* 上記の準備をコンテナ内で実行するには、コンテナ内でユーザー指定のコマンドを実行するだけでは足りません
  * コンテナに`git`コマンドが入っていない場合でも指定Gitからソースコードを取得したい
  * ユーザー指定コマンドの完了時に、リーターンコードに応じたKAMONOHASHIのAPIをたたく

* ユーザーにgitなどの必要ツールをインストールさせるdocker buildをさせることは避けたいです
  * 新しいツールが必要になったらユーザーにすべてビルドし直しをお願いするなどの事態が起きえます

ということで、ユーザーはgitなどの入っていないコンテナを機械学習実行コンテナに指定していても
git cloneなどを実行してコンテナ内にソースコードを取り込む、ということを実現する必要があります

# 上記の実現方法
## 概要
* 「Gitからソースコード取得」については次で実現
  * initContainersでソースコード取得
* 「コマンドの完了時にKAMONOHASHIのAPIをたたく」については次で実現[^2]
  * サイドカーコンテナがユーザー指定コマンドの完了を待機
  * ユーザー指定コマンド前後にKAMONOHASHIの処理を挿入し、サイドカーにリターンコードを通知
  * リターンコードに応じ、サイドカーコンテナ内蔵のKAMONOHASHI CLIをたたく
    * CLIの内部でAPIがたたかれる 

## 「Gitからソースコード取得」の仕組み
### 概要
* init ContainersはKubernetesの機能です。
  * Pod内のメインのコンテナ(ユーザー指定コンテナ)が立ち上がる前に、別のコンテナを実行することができます
  * [マニュアル](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/)

* init Containersとメインコンテナの間のデータの受け渡しにはEmptyDirボリュームを使用します
  * EmptyDirボリュームはPod作成時に空のディレクトリが確保され、Pod削除時に消されるというボリュームです
  * Pod内のコンテナ間でのデータ受け渡しに使えます
  * [マニュアル](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir)

### フロー
* git入りのコンテナをinit Containersで起動します。
  *  EmptyDirをマウントします
  * `git clone`でEmptyDir内にソースコードを取り込みます
* init Containersが終了するとメインのコンテナが立ち上がります
  * initContainersと同じEmptyDirをマウントして中にあるソースコードを利用します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/61b87751-bf6f-8137-9ea0-dc86df4a5d28.png)

## 「コマンドの完了時にKAMONOHASHIのAPIをたたく」の仕組み
### 概要
* Podには複数のコンテナを含めることができます
* ユーザーの指定したコンテナと同時にKAMONOHASHIのCLIを内蔵したコンテナを起動します（サイドカー）
* EmptyDirを通じて、メインコンテナの終了コードをファイル書き込みでサイドカーに通知します
  * Kubernetesに投げるyamlを組み立てる段階で、ユーザー指定コマンドのリターンコードを受け取ってファイル書き込みする処理を実行コマンドに指定します[^3]
* サイドカーはファイル書き込みを待機しています。ファイルに書き込みがあると、KAMONOHASHIのCLIのコマンドを実行して、サーバーのAPIをたたきます

### フロー
* ユーザー指定コマンドのリターンコードをEmptyDirのファイルに書き込みするように実行コマンドを組み立てる
* ユーザー指定コンテナ、サイドカーコンテナが起動
* サイドカーコンテナがinotifywaitコマンドを実行し、EmptyDir内のファイル書き込みを待機
* ユーザー指定コマンドが実行され、ファイルにリターンコードが書き込まれる[^4]
* サイドカーのinotifywaitから制御が返る。EmptyDir内のファイルからリターンコードを読み取ってKAMONOHASHI CLIのコマンド実行
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/8c2dee57-b25a-eeae-0968-fd5ab5357099.png)


# まとめ
上記の方法により、KAMONOHASHIで必要となるgitやCLIツールを、ユーザーの指定するコンテナにインストールせずに使うことができます。
これによりKAMONOHASHI用のコンテナビルドなどをせずに済み、DockerHubなどから自由にコンテナを使えるようになります

# 関連
[イメージビルドせずにKubernetes機能だけでがんばる](https://qiita.com/ozota/items/6c4fb310bb4a3390b5fa)

[^1]: KAMONOHASHIのサーバー側でKubernetesのAPIをたたいて取得してもよいのですが、アプリエンジニアがポーリングが嫌だということでコンテナから通知する仕組みを用意しました
[^2]: pod終了時に処理を行う方法としてはpod lifecycleのpreStopでのhttpGetもありますが、おそらくリターンコードの分岐ができません。ユーザー指定コンテナ内でcurlなどを使う方法も考えられますが、それらのないコンテナもありえることと、ヘッダーやボディーの組み立てが面倒なため避けました
[^3]: 実際にはconfigMapでスクリプトを配布する仕組みを実装していて、ユーザー指定コマンドはそのスクリプトの中に埋め込んでいます。k8sに投げるpodのyamlの実行コマンドではそのスクリプトを実行させています。
[^4]: 実際にはサイドカーのinotifywait実行前にユーザーコマンドが終わってしまうケースも想定し、サイドカーがリターンコード生成用ファイルを作成するのをビジーループで待つ処理をユーザー指定コマンド実行前に挿入しています