DockerでJupyterhubを試してみた

# はじめに

マルチユーザプラットフォームを作れるJupyterhubを試してみたときの自分用メモ。

# やること

- Jupyterhub起動
- jupyterhub_config.pyの設定
- NativeAuthenticatorの導入とお試し

# 環境

```
$sw_vers
ProductName:	Mac OS X
ProductVersion:	10.13.6
BuildVersion:	17G14042
```

```
$docker --version
Docker version 20.10.0, build 7287ab3
```

# やったこと

## Jupyterhubの起動

[公式のドキュメント](https://jupyterhub.readthedocs.io/en/latest/quickstart-docker.html#alternate-installation-using-docker)に記載されているように、次のdockerのコマンドでJupyterhubを起動できます。

```bash

docker run -d -p 8000:8000 --name jupyterhub jupyterhub/jupyterhub jupyterhub
```

ただ、このままだと何も設定もできていないし、ログインしてもほとんど何もできない状態なので、コンテナ内で設定をしていく必要があります。

## jupyterhub_config.pyの設定

先ほど作ったコンテナは一度削除して改めてコンテナを起動して中に入ります。

```bash

docker run -it -p 8000:8000 --name jupyterhub jupyterhub/jupyterhub bash
```

必要なライブラリなどをインストールしていきます。

```bash
apt update
apt upgrade
apt install python3 python3-pip npm nodejs libnode64 vim git
npm install -g configurable-http-proxy
```

次のコマンドでjupyterhub_config.pyを作成します。

```bash
jupyterhub --generate-config
```

作成されたjupyterhub_config.pyに以下の行を追加します。

```python:jupyterhub_config.py
# ログイン後に http://...:8000/user/<username>/lab? へ遷移する設定（Jupyterlabが起動）
c.Spawner.default_url = '/lab'
# Jupyterlabで作成されたノートブックファイルなどが格納されるディレクトリ
c.Spawner.notebook_dir = '~/notebook'
# adminユーザのユーザ名
c.Authenticator.admin_users = {'testadmin'}
# ログインが許可されているユーザ名
c.Authenticator.allowed_users = {'testuser01'}
```

デフォルトだと認証には`PAMAuthenticator`を使用するようになっていて、OSのユーザ＆パスワードでログインするようになっています。
動作確認用に testadmin と testuser01 というユーザを作成しておきます。

```bash
adduser testuser01
mkdir -p -m 777 /home/testuser01/notebook
chown testuser01: /home/testuser01/notebook

adduser testadmin
mkdir -p -m 777 /home/testadmin/notebook
chown testadmin: /home/testadmin/notebook
```

設定ファイルを指定してJupyterhubを起動します。

```bash

jupyterhub jupyterhub_config.py 
```

デフォルトだと`http://localhost:8000`でJupyterhubが起動するので、ブラウザでアクセスします。

![jupyterhub_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/4dd7cbc8-d8e0-4363-2663-4fe85518b0b0.png)

先ほど作ったtestuser01でログインするとJupterlabの画面が表示されて、Pythonが動くと思います。

![jupyterlab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/94a52069-e399-9ace-28d7-98899e24bf99.png)

## Jupyterhubの「Add Users」の特徴

次にadminユーザとして作ったtestadminとしてログインしてみます。

![jupyterhub_admin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/9e5c850d-c430-7229-c659-09dab8051324.png)

testadminとしてログイン後、Jupyterlabの「File」→ 「Hub Control Panel」でJupyterhubの管理画面に遷移できます。
ここで、「Admin」 → 「Add Users」を押すとユーザを追加する画面が出てきます。
ただ、パスワードを設定できるようなところもなく、新しく作ったユーザ名＋空のパスワードでもログインできません。
「あれ？」と思って、サーバーのログを見てみると、

```bash

[I 2021-02-11 13:39:09.631 JupyterHub auth:897] Creating user: adduser -q --gecos '""' --disabled-password testuser02
```

と表示されていて、AuthenticatorがOSのユーザを作成しているものの、パスワードで認証できないようになっています。
デフォルトのままだと、管理者はOSのコマンドでいちいちユーザを作らないといけない感じになります。

## NativeAuthenticatorの導入

管理者ユーザであってもGUIでユーザの追加などを行いたいなと思ったので、
[NativeAuthenticator](https://github.com/jupyterhub/nativeauthenticator)を導入します。

```bash

mkdir native_authenticator
cd native_authenticator/
git clone https://github.com/jupyterhub/nativeauthenticator.git
cd nativeauthenticator/
pip3 install -e .
```

インストールは以上で終わりです。
あとは`jupyterhub_config.py`に設定するために下の行を追加します。

```python:jupyterhub_config.py
c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
```

再びJupyterhubを起動すると最初のログイン画面にSingupへのリンクが現れて、
自分でユーザ名とパスワードを入力できるようになります。

![jupyterhub_nativeauth_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/61d476f3-a7a5-8f3c-1203-719231bdee33.png)

## NativeAuthenticatorで最初にログインするとき

NativeAuthenticatorだとユーザ名＆パスワード情報はOSのユーザ情報を見に行かないです。
デフォルトだとユーザ登録時に管理者の許可が必要なので、管理者としてユーザ登録しようとしても許可が必要になるのでは？最初にどうやってログインするの？って話なのですが、
`jupyterhub_config.py`の`c.Authenticator.admin_users`に記載したユーザ名であれば、許可のフローをスキップしてログインできます。
ということで

1. `jupyterhub_config.py`の`c.Authenticator.admin_users`にユーザ名を追加する。（今回はtestadmin）
2. Jupyterhubのサインアップ画面でユーザ名testadmin、適当なパスワードで登録
3. Jupyterhubのログイン画面に行き、ユーザ名testadmin、入力したパスワードでログイン

という手順を踏むと管理者でログインできます。

## NativeAuthenticatorのユーザ追加時の特徴

あるユーザでSignupしようとユーザ名とパスワードを入力すると、管理者の許可待ちになります。
（なお、NativeAuthenticatorの`open_signup`を`True`に設定していれば（デフォルトは`False`）、誰でもユーザを作成してログインできます。）

違うブラウザで管理者としてログインし、`http:localhost:8000/hub/authorize`にアクセスします。（ボタンなどがないので、URLを直打ちします。不便...）

![jupyterhub_native_authorize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/c4809cac-5dfd-b186-6bbb-b7e1503d35c4.png)

ここで新しいユーザに対して「Authorize」をクリックします。
他のブラウザでtestuser10としてログインしようとすると...エラー。

![jupyterhub_native_adduser_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/37947942-2c2d-0414-81c0-7c868f2d83f9.png)

このときのサーバーのログは次の通り。

```bash

[W 2021-02-12 12:50:41.671 JupyterHub auth:503] User 'testuser10' not allowed.
[W 2021-02-12 12:50:41.673 JupyterHub base:763] Failed login for testuser10
```

NativeAuthenticatorのソースコードをよく見てみると最初にユーザを作成する際に、ユーザ名が`jupyterhub_config.py`の`c.Authenticator.allowed_users`に含まれているかチェックしていて、`False`の状態のままだとログイン時にはじかれてしまうロジックになっています。
https://github.com/jupyterhub/nativeauthenticator/blob/b0d9f9e678af08ae36b50be96e533b7a98d227bf/nativeauthenticator/nativeauthenticator.py#L202

これを解消するには、「Authorize」した後に、「Admin」 → 「Add Users」でユーザを追加すればよいです。

![jupyterhub_admin_adduser_testuser10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/2611caa7-f4c9-cb76-866d-026015109820.png)

## pre_spawn_hookの設定

上記の操作でログインはできますが、ログイン後にエラーになります。
というのも、Jupyterlabで`~/notebook`のディレクトリを見に行きますが、それが存在しないからです。

また、NativeAuthenticatorではOSのユーザを作成しないです。
念のため、`/etc/passwd`を確認すると、

```bash:

cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
testuser01:x:1000:1000:,,,:/home/testuser01:/bin/bash
testadmin:x:1001:1001:,,,:/home/testadmin:/bin/bash
testuser02:x:1002:1002:"",,,:/home/testuser02:/bin/bash
hoge:x:1003:1003:"",,,:/home/hoge:/bin/bash
testuser03:x:1004:1004:"",,,:/home/testuser03:/bin/bash
testuser04:x:1005:1005:"",,,:/home/testuser04:/bin/bash
```

こちらの通り、testuser10が存在しないです。

ということで、Jupyterlabを開始する前（Spawnの前）に

- OSにユーザを作っておく
- ディレクトリを作っておく

という操作が必要ですので、`pre_spawn_hook`を設定します。
`jupyterhub_config.py`に以下を追加します。

```python:jupyterhub_config.py
import pwd, subprocess
def pre_spawn_hook(spawner):
    username = spawner.user.name
    try:
        pwd.getpwnam(username)
    except KeyError:
        subprocess.check_call(['sh', '/srv/jupyterhub/bootstrap-script/bootstrap.sh', username])

c.Spawner.pre_spawn_hook = pre_spawn_hook
```

また、上記の操作を実行するためのシェルスクリプトを作成します。

```bash:/srv/jupyterhub/bootstrap-script/bootstrap.sh
# adding user and making the user directory
adduser -q --gecos '""' --disabled-password $1
mkdir -p -m 777 /home/$1/notebook
chown $1: /home/$1/notebook
```

再度、Jupyterhubを起動します。
testuser10でログインすると...

![jupyterhub_testuser10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/846d119f-146e-21a5-a91a-8d44ee321327.png)

無事にJupyterlabが表示されました！
一番最後に`jupyterhub_config.py`のコード全量を載せておきます。

# まとめ

- Jupyterhubはdockerですぐに起動できるものの、使うためには設定が必要
- デフォルトだとOSのユーザで認証
- JupyterhubのAdminで追加できるユーザは（OSの）パスワード設定不可
- NativeAuthenticatorでサインアップ、ログインするには以下の2つが必要
    - 管理者によるAuthorize & Add Usersの操作
    - pre_spawn_hookによるOSユーザ & ディレクトリ作成


こうやればもっと楽にできるよ！などコメントあればお願いします！

# 参考にした記事など

- https://www.hatarilabs.com/ih-en/how-to-set-a-multiuser-jupyterlab-server-with-jupyterhub-in-windows-with-docker
- https://qiita.com/noguhiro2002/items/99758ad66587f24c0ef2
- https://qiita.com/kitoo/items/9b9ac98c0d059bd1ab1f


# jupyterhub_config.py

```python:jupyterhub_config.py
c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
c.Spawner.default_url = '/lab'
c.Spawner.notebook_dir = '~/notebook'
import pwd, subprocess
def pre_spawn_hook(spawner):
    username = spawner.user.name
    try:
        pwd.getpwnam(username)
    except KeyError:
        subprocess.check_call(['sh', '/srv/jupyterhub/bootstrap-script/bootstrap.sh', username])

c.Spawner.pre_spawn_hook = pre_spawn_hook
c.Authenticator.admin_users = {'testadmin'}
c.Authenticator.allowed_users = {'testuser01'}

```