JupyterHub + dockerspawnerでユーザごとにマウントするボリュームを変える

# JupyterHub + dockerspawnerでユーザごとにマウントするボリュームを変える
## はじめに
Jupyter Notebook便利ですよね。
Jupyter Hubを使うと簡単にマルチユーザ環境も構築できて、ちょっとやって見る用途だと簡単にいい感じの環境が作れます。
そしてdockerspawnerを使えばユーザ単位で空間を分離できるし、あれやこれやできて大変便利な環境は作れます。

しかし、ユーザ単位で設定を切り替える、例えばプロジェクトAに所属するユーザAとユーザBだけ特定のディレクトリを見せる等を考え出すととたんに標準的に用意されている機能だけではうまくいく気がしなくなってきます。

ここでは、どのようにしてユーザ単位での処理を制御するのかを記載します。

## 前提条件

- Docker
  - 適切にインストール([Ubuntuならこれ](https://docs.docker.com/install/linux/docker-ce/ubuntu/))されていること

- JupyterHub
  - 適切に[インストール](https://github.com/jupyterhub/jupyterhub)されていること

## デザイン
[dockerdspawner](https://github.com/jupyterhub/dockerspawner)を利用して個々のユーザの環境を分離します。
また、ユーザ認証はPAM認証(Linuxローカルユーザ)を利用して、ユーザの区別はユーザグループにて行うことにしました。
今回はproject-で始まるグループを持っているユーザのみはそのグループ名に該当するボリュームを割り当てることにします。
例えばproject-Aで始まるユーザはproject-Aという名前のボリュームをマウントします。

また、コンテナ内のディレクトリ設計は以下の通りとします。

| # | Directory                 | 説明                          |
|:--|:--------------------------|:------------------------------|
| 1 | /root/workspaces/         | notebookのルートディレクトリ        |
| 2 | /root/workspaces/personal | 個人用ディレクトリ(volumeをマウント)  |
| 3 | /root/workspaces/project  | プロジェクト単位の共有ディレクトリを設置 |

プロジェクト単位の共有ディレクトリは、例えばプロジェクト名がproject-Xだった場合、/root/workspaces/project/Xのような形でマウントされることにします。

## 構築
### Dockerコンテナの用意
上記に従って、Dockerコンテナを用意します。
ベースイメージとして、jupyter/scipy-notebookを利用しますが、jovyanユーザだといろいろ面倒なのでrootユーザに切り替えちゃってます。

以下、同等のものを[gist](https://gist.github.com/marufeuille/c9c6507e95eedf6d861903d527343421)においてあります。

```Dockerfile
FROM jupyter/scipy-notebook

USER root
RUN mkdir -p /root/workspace/personal /root/workspace/project
WORKDIR /root/workspace
CMD ["jupyterhub-singleuser", "--allow-root"]
```

jupyterhub(のdockerspawner)から利用する場合、最後のCMDはほぼ固定的なことに注意してください。

buildは以下のように実施してください。

```bash
docker build -t TAGNAME .
```

ここで指定したTAGNAMEをjupyterhubのconfigで指定します。

### Linuxユーザ&グループの用意

今回は試しに以下設定とします。

| # | User   | Group(デフォルト以外)   |
|:--|:-------|:--------------------|
| 1 | user-a | project-a           |
| 2 | user-b | project-a,project-b |
| 3 | user-c | -                   |

ユーザ数もグループ数も大したことがないので、ここは淡々と作成します。

```bash:ユーザ作成
adduser user-a
adduser user-b
adduser user-c
```

```bash:グループ作成
groupadd project-a
groupadd project-b
```

```bash:ユーザをグループへ追加
usermod -aG project-a user-a
usermod -aG project-a user-b
usermod -aG project-b user-b
```

### dockerの前準備
#### ネットワークの準備
jupyterhub + dockerspawnerを使うとき、docker networkのどこに所属させるかを指定する必要があります。
ここでは「jupyter-network」という名前のネットワークを作成しています。

```bash:ネットワークを作成
docker network create jupyter-network
```

私はここにこだわりはなかったのでデフォルトのままですが、もっと細かい条件を設定しても大丈夫なはずです。

#### ボリュームの用意

グループに対応するボリュームを作成します。

```bash
docker volume create project-a
docker volume create project-b
```

ちなみに、ボリュームを作らないとデフォルトの場所に勝手に作ってくれるので、実はこの手順は飛ばしてしまっても大丈夫です。
もし、デフォルトではない場所、例えばNFSなどの共有ディスク上に作りたい場合は個々で作成しておいたほうが良いでしょう。

#### 関連するパッケージのインストール

```bash
pip3 install jupyter_client dockerspawner
```

#### Jupyterのコンフィグ作成

ここがキモです。
ふつうにやったらできないことなので、DockerSpawnerの継承したMyDockerSpawnerというクラスを作成し、startメソッドをoverrideして、そこに処理を書き込みます。
その処理が終わったら元のstartメソッドを呼び出す必要があるため、最後に「return super().start()」を呼び出します。

```python:jupyterhub_config.py(抜粋)
from dockerspawner import DockerSpawner
import pwd, os, grp
class MyDockerSpawner(DockerSpawner):
    def start(self):
        # ユーザごとにマウントするボリューム処理ここから
        name = self.user.name
        user_data = pwd.getpwnam(name)
        gid_list = os.getgrouplist(name, user_data.pw_gid)

        self.volumes['jupyterhub-user-{username}'] = notebook_dir + '/personal'

        for gid in gid_list:
            gname = grp.getgrgid(gid).gr_name
            if gname.startswith("project-"):
                dirname = gname.replace("project-", "")
                self.volumes[gname] = notebook_dir + "/project/" + dirname
        # ここまで
        return super().start()

c.JupyterHub.spawner_class = MyDockerSpawner
```

処理自体はなんてことないですね。
単純にユーザ名からグループ名のリストを取得し、その中からproject-ではじまるものがあったらself.volumesにマウントするボリュームを追加しています。
作成したクラスを「c.JupyterHub.spawner_class」に指定します。

フルのコンフィグは[ここ](https://gist.github.com/marufeuille/596cf6f9e26fa6fcb7e304d5b163c7c8)においてあります。

## 起動とテスト
### 起動

起動は通常のJupyterHubと同じです。以下のようにしてください。

```bash
jupyterhub --config jupyterhub_config.py
```

### テスト

各ユーザでログインして、project配下を確認してください。

- user-a
  - aディレクトリのみ見えている
![user-a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99704/343fc2d5-59a0-63f6-c992-45c665309413.png)


- user-b
    - a, b両方が見えている
![user-b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99704/3eafe75c-317b-9148-8a60-6cf6b005eb03.png)


- user-c
    - aもbも見えていない
![user-c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99704/0adf7710-3e4f-3eec-e7ac-13167b00cd0e.png)

## 最後に
いかがでしたでしょうか。
少しだけpythonのコードを書かなくてはいけないですが、Jupyter使う方からすれば特に問題ないレベルの操作なのかなと思っており、非常に簡単にユーザ単位の制御ができることがわかると思います。

今回はグループごとにディレクトリをつけるという話でしたが、それ以外にも色々な使い方ができると思うので是非参考にご活用ください。