Kubeflow Notebooks向けにsudoできるカスタムイメージを作る

# 要約
- Kubeflow Notebooksで使えるオリジナルのコンテナイメージ(カスタムイメージ)の作成例です
- コンテナイメージならなんでも使える訳ではなく、Kubeflow独自の条件をクリアする必要があり少々ハマりました

# ことの経緯
**Kubeflow Notebooks**ではNotebookインスタンスを払い出す際にデフォルトで使えるいくつかのコンテナイメージが組み込まれています

![スクリーンショット 2022-11-28 9.54.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436161/bda24494-4d82-ef96-3ed7-38584199e874.png)


ただ用途によってはライブラリが足りないなど、やはりデフォルトのイメージだけではちょっと物足りなくなる事があります。
また上記のKubeflowデフォルトのイメージでは総じて**jovyan**という名前の謎のユーザを使う事が強制されるのですが**何より困るのがこのjovyan、sudoできないのである**。
「まぁちょっとお試しで使うだけのライブラリだから…」というノリで```sudo apt-get install```することができません。

```bash
(base) jovyan@lab-0:~$ apt-get install hoge
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?

(base) jovyan@lab-0:~$ sudo apt-get install hoge
bash: sudo: command not found
```

幸いにもKubeflow Notebooksでは**ユーザ独自のコンテナイメージ**(=Custom Image)を使う事もできるので、必要なライブラリをかき集めつつ、ついでにsudoできるコンテナイメージを作ってみました。

# カスタムイメージの要件
コンテナイメージならなんでもKubeflow Notebooksから扱える訳ではなく、クリアすべき条件がいくつかあります。

https://v1-6-branch.kubeflow.org/docs/components/notebooks/container-images/#image-requirements

以下、意訳です。
- ポート```8888```でHTTPインターフェースを公開すること
    - Kubeflow Notebook実行時に環境変数```NB_PREFIX```にコンテナが待ち受けるべきURLパスが設定される
        - (※訳注)Kubeflow Notebookからコンテナアプリケーションにアクセスする際、ドキュメントルート(/)など静的なパス宛てではなく動的に生成されるパスに対してアクセスが行われます(今回はここでハマりました)
        - この動的なパスはNotebookインスタンスを払い出す際に環境変数```NB_PREFIX```に設定されるため、アプリケーション側でこの環境変数を使って動的にコンテンツを生成してあげる必要があります
            ```
            # NB_PREFIXの実例
            $ kubectl get pod -n kubeflow-user-example-com test-0 -o yaml | grep NB_PREFIX -A 1
            - name: NB_PREFIX
              value: /notebook/kubeflow-user-example-com/test
            ```
    - Kubeflow NotebookはIFrameを利用してコンテナアプリケーションのコンテンツを投影するため、アプリケーション側で```Access-Control-Allow-Origin: *```をHTTPレスポンスヘッダに設定しておくこと
- jovyanというユーザー権限で実行すること
    - jovyanのホームディレクトリは```/home/jovyan```であること
    - jovyanのUIDは```1000```であること
- Kubeflow Notebooksでは/home/jovyanに空のPVCがマウントされた状態で起動される
    - kubeflowはPodの再起動後も状態を保持するために```/home/jovyan```にPVCをマウントする
    - (※訳注)ここちょっと趣旨が読み取りにくいですが「```/home/jovyan```にはPVCがマウントされることになるのでホームディレクトリは空っぽにしておいてね」ぐらいの意味かと思われます。

# カスタムイメージを作ってみる
上記の条件をクリアするコンテナイメージの作成例です。
## Dockerfile
```Dockerfile
FROM tensorflow/tensorflow:2.11.0-jupyter

ARG DOCKER_UID=1000
ARG DOCKER_GID=100
ARG DOCKER_USER=jovyan
ARG DOCKER_PASSWORD=jovyan

# ポイントその1: jovyanという名前のユーザを作成
# ポイントその2: jovyanのUIDは"1000"を指定
# ポイントその3: jovyanをsudoグループに追加
RUN useradd -m --uid ${DOCKER_UID} -g ${DOCKER_GID} --groups sudo ${DOCKER_USER} \
  && echo ${DOCKER_USER}:${DOCKER_PASSWORD} | chpasswd

# ポイントその4: sudoをインストールしておく
RUN apt-get update && apt-get install -y --no-install-recommends \
        sudo \
        git \
        wget \
        libatlas-base-dev \
        libgl1-mesa-dev \
        libopencv-dev

# pipでインストールするライブラリ群
COPY --chown=jovyan:users requirements.txt /tmp/requirements.txt
RUN python3 -m pip install -r /tmp/requirements.txt --quiet --no-cache-dir \
 && rm -f /tmp/requirements.txt

# ポイントその5: jovyan権限で実行
USER ${DOCKER_USER}

# ポイントその6: 8888番ポートを公開
EXPOSE 8888

# ポイントその7: Access-Control-Allow-Originを設定(--NotebookApp.allow_origin='*')
# ポイントその8: NotebookサーバのベースURLを環境変数"NB_PREFIX"で指定(--NotebookApp.base_url=${NB_PREFIX})
ENTRYPOINT /usr/bin/python3 /usr/local/bin/jupyter-notebook --notebook-dir=/home/${DOCKER_USER} --ip 0.0.0.0 --no-browser --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.allow_origin='*' --NotebookApp.base_url=${NB_PREFIX}
```

- ベースイメージはDockerHubで公開されている[tensorflow-notebook](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#jupyter-tensorflow-notebook)のCPU版を使っています
- 上記でポイントとして記載した8点を押さえておけば、その他はお使いの環境や用途に応じてカスタマイズできるはずです。
- 設定は任意ですが、ENTRYPOINTに記載した```--NotebookApp.token```と```--NotebookApp.password```を空にしておけばNotebookサーバを開いた際の認証を無効にすることができます(Kubeflow Dashboradにログインする際にも認証されるので二重に認証されるとめんどくさい)

## requirements.txt
pipでインストールするライブラリのリストですが、今回の本筋とは関係ありませんのでご参考までに。
```
pandas
matplotlib
numpy
opencv-python
tqdm
```

## コンテナイメージのビルド
あとはイメージをビルドして任意のリポジトリに格納します。
```bash
$ sudo docker build . -t hoge/fuga:v1.0
$ sudo docker push hoge/fuga:v1.0
```

# カスタムイメージをKubeflow Notebooksで使ってみる
上記でビルドしたカスタムイメージをKubwflow Notebookから使ってみます。
New notebookの画面で```Custom Image```の欄にチェックを入れ、作成したコンテナイメージのパスを記入します。
以降の設定は通常のNotebook作成時と特に変わりません。
![スクリーンショット 2022-11-26 2.03.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436161/fc724aea-da57-9b47-745d-0bda897ddda3.png)


Notebookサーバが起動したらターミナルを開いてsudoのテスト

![スクリーンショット 2022-11-26 2.12.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436161/cc4b3aa6-e319-5a33-ce83-e6c7e66e850c.png)

sudoできたので、目的達成です。お疲れ様でした。

# 参考資料
https://github.com/jupyter/docker-stacks/issues/408


https://github.com/kubeflow/kubeflow/issues/5516


https://github.com/kubeflow/kubeflow/issues/3821