AWSでJupyterHub (JupyterHubのセットアップ)

# 概要

全体概要は，[AWSでJupyterHub (概要) ](https://qiita.com/ryouka0122/items/af5004d17e0ac934e550)にあるので，参照してください．

今回は，前回インストールしたAnacondaを使ってJupyterHubを組み立てていきます．
また，JupyterHub自体にWebサーバの機能を持っていますが，今回はnginxをWebサーバにおいて，その後ろにJupyterHubを置くような構成にしていきます．

# おしながき
* JupyterHub用ユーザの作成
* JupyterHubのインストール
* JupyterHubの設定
    * jupyterhub-config.pyの中身
* JupyterHubの起動
* nginxとの接続

前回同様，今回も基本的に`ec2-user`で操作していきます．
細かい説明を入れていくので，記事としては長めになっていますのでご了承ください <(_ _)>


# JupyterHub用ユーザの作成
インストールしただけでは，複数ユーザで利用することが出来ないです．また，目的に合わせて権限周りは設定していくべきです．
なので今回は，「jupyterhubグループに属するユーザが利用できる」ように設定しておきます．
次のように設定していきます．

* jupyterhubグループの追加
* `ec2-user`にjupyterhubを追加
* `/opt/anaconda3`配下のグループをjupyterhubに変更
* anacondaの確認

`ec2-user`へのグループ追加は便宜上，設定しておくものです．きちんと作業用ユーザを用意している場合は，そのユーザに対して追加するほうがよいでしょう．

`groupadd`を使って`jupyterhub`グループを作成して，`gpasswd`を使って`ec2-user`にグループ追加していきます．
以下の画像のような流れになってます．
グループが付いているか確認するには，`id {username}`で確認できます．
※グループ追加したての時は，その接続では有効になっていないので，設定が終わったら再ログインして設定を反映させたほうが良いです

![グループ追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165030/5deea07f-993c-3837-8fcf-cf971832b154.png)

`/opt/anaconda3`のグループを変更していきます．
変更は以下のコマンドになります．
権限もotherに与えないように`770`にしておきます．

```
chgrp -R jupyterhub /opt/anaconda3
chmod 770 -R /opt/anaconda3
```

## JupyterHub実行ユーザの追加
`ec2-user`でJupyterHubを動かしてもいいのですが，せっかくなので専用ユーザで実行するようにします．
グループは先ほど作ったので，それを割り当てるようにします．
JupyterHubだけ動かすアプリユーザということで，ログインできないように`nologin`をつけておきます．

```
sudo useradd -s /sbin/nologin jupyterhub -g jupyterhub
```

`id jupyterhub`と入力すれば，jupyterhubユーザの情報が出力されて作成されたことを確認できます．

## Anacondaの確認
`ec2-user`で`/opt/anaconda3/bin/conda`とオプションなしで打ち込むと，「( ﾟДﾟ)＜指定しろや！」ってヘルプ情報をずらーっと出してきます．出てくれば，権限や設定周りがうまくいってそうだということで次に進みます．

### パスを通す
今回は特に気にしないのですが，普段から`conda`コマンドを利用する場合は，`/opt/anaconda3/bin`にパスを通しておくと何かと便利です．
ホームディレクトリに移動して，`.bashrc`を編集ツールを使って，最後に以下を追記して保存します．

```
export PATH=/opt/anaconda3/bin:$PATH
```

追加出来たら，再ログインするか `source .bashrc` で設定を反映させます．

# JupyterHubのインストール
さっそく，JupyterHubをインストールしていきます．
インストールは以下のコマンドを使います．

```bash
/opt/anaconda3/bin/conda install -c conda-forge jupyterhub

# パスを通していれば，こちらでもOK
conda install -c conda-forge jupyterhub
```

実行すると，リポジトリから探してきて，インストールが必要なパッケージをリストアップしてくれます．
「インストールしてよいか？」と聞かれるので，インストールさせてあげましょう．
しばらく作業していますが，そのうち終わります．
インストール完了したら，以下のコマンドで確認します．

```
/opt/anaconda3/bin/conda list
```

このコマンドはAnacondaにインストールされたパッケージを出力してくれます．
このなかに「jupyterhub」のパッケージがあれば，成功しています．
numpyやscikit-learnなど機械学習でよく使われるパッケージはあらかじめインストールされていますが，リストになくて必要なものがあれば，同様にパッケージをインストールしていけば良いのですが，Anacondaは環境ごとにパッケージ管理できるので，この方法でないほうが良いこともあります．（バージョン固定させたいときなど）

# JupyterHubの設定
JupyterHubのインストールが終われば，次は設定を行っていきます．
最初は設定ファイルがないので，設定ファイルを作るところから始めていきます．
まず次のコマンドを実行して，設定ファイルを置く場所を作ります．

```bash
sudo mkdir /etc/jupyterhub
sudo chmod 770 /etc/jupyterhub
sudo chown jupyterhub:jupyterhub /etc/jupyterhub
```

次に，以下のコマンドで，設定ファイルを作ります．


```bash
cd /etc/jupyterhub/
sudo -u jupyterhub /opt/anaconda3/bin/jupyterhub --generate-config
```

実行すると，`Writing default config to: jupyterhub_config.py` と出力されればカレントディレクトリに設定ファイルが出力されています．
もしディレクトリにwrite権限がないと，エラーログが出力されます．
今，出力されたファイルはバックアップも兼ねてデフォルトファイルとしておきます．（しなくてもいいのですが念のため）

```bash
sudo -u jupyterhub mv /etc/jupyterhub/jupyterhub_config.py /etc/jupyterhub/jupyterhub_config.py.default
```
バックアップ用ファイルが出来たらさっそく，`jupyterhub-config.py`の作成に取り掛かります．
認証方式は，いろいろあるのですが，今回はLinuxユーザがJupyterHubユーザとして利用するようにしていきます．（ほんとはLDAP認証したかったけどね．．．）




## jupyterhub-config.pyの中身
以下のような設定にします．ファイルはPythonなので，Pythonの文法であればどんな書き方でも大丈夫です．
不要な行は`#`を使ってコメントアウトすることが出来ます．

```python:/etc/jupyterhub/jupyterhub-config.py
c.JupyterHub.bind_url = 'http://127.0.0.1:8080'

c.Spawner.notebook_dir = '~/notebook'
# c.Spawner.notebook_dir = '/mnt/jupyterhub/{username}/notebook'
# c.Spawner.default_url = '/lab'

c.Authenticator.admin_users = {
  'ec2-user'
}

c.Authenticator.whitelist = {
  'ec2-user',
  'testuser'
}
```

|項目|型|概要|設定例|
|:--|:--|:--|:--|
|c.JupyterHub.bind_url|str|JupyterHubがリクエストを受け取るためのURLです．ポート番号もここで指定します．|`http://127.0.0.:8080`|
|c.Spawner.notebook_dir|str|`JupyterHub`が利用するnotebookデータのディレクトリを指定します．これはユーザごとに管理されます．`~`はユーザのホームディレクトリを表していて，`{username}`とするとユーザ名に置き換わるようになります．（プレスホルダー機能）|`~/notebook`|
|c.Spawner.default_url|str|`c.JupyterHub.bind_url`の後ろにつけるURLです．上記のコードでは`http://127.0.0.1:8080/lab`というようになります．この項目を利用することで，既存システムに追加できるようになっています．`lab`とすると`notebook`から`JupyterLab`に変わります．|`/lab`|
|c.Authenticator.admin_users|set|管理者権限を持つユーザの一覧です．空にすると，管理者がいない状態になります．||
|c.Authenticator.whitelist|set|この一覧にあるユーザだけログインすることが出来るようになります．||

設定ファイルが出来たら，`ec2-user`のホームディレクトリに`notebook`というディレクトリを作っておきましょう．

```
mkdir ~/notebook
```

# 不足しているものをインストール
設定ファイルを作るだけではなく，JupyterHubを起動する際に`npm`と`configurable-http-proxy`が必要なので，追加でインストールしていきます．

## npmのインストール
```
# リポジトリの取得
curl -sL https://rpm.nodesource.com/setup_8.x | sudo bash -

# nodejsのインストール
sudo yum install -y nodejs

# 確認
npm -v
```

## configurable-http-proxyのインストール
```
sudo npm install -g configurable-http-proxy
```


# JupyterHubに接続してみる
ということで，JupyterHubを起動してみます．
起動コマンドは以下のような感じになります．

```
cd /etc/jupyterhub
/opt/anaconda3/bin/jupyterhub
```

起動すると起動ログと一緒にエラーが出てきます．
エラーの内容が` Error adding user testuser already in db` というものであれば，「`testuser`ってユーザがシステムにないよ？」といっているので，ひとまず無視して問題ないです．（後で足していくので）
nginxとつなげるので，`Ctrl + C`で止めておきます．

#nginxとの接続
[AWSでJupyterHub (nginx)](https://qiita.com/ryouka0122/private/a8c4bb12b6a958b3da04) で後回しにしていたnginxの設定ファイルに手を加えていきます．
`/etc/nginx/nginx.conf`にある設定を以下のようにlistenの部分をコメントアウトします．

```text:/etc/nginx/nginx.conf

...（省略）...

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
#  ★★ 以下2行のlistenをコメントアウトする ★★
#        listen       80 default_server;
#        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

...（省略）...

```

次に，`/etc/nginx/conf.d/`に移動します．このディレクトリは初めは空っぽで，`/etc/nginx.conf`が参照するようになっています．
ここに，以下の設定情報を書いた`jupyterhub.conf`というファイルを作ります．
通常のproxyの設定に合わせて，JupyterHubがwebsocketを利用していることから，それに合わせた設定も追加しておきます．

```text:/etc/nginx/conf.d/jupyterhub.conf

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen       80 default_server;
    server_name  xxx.yyy.zzz;                              # ←ここにJupyterHubに接続するためのドメインを指定する
    root         /usr/share/nginx/html;

    access_log /var/log/nginx/jupyterhub.log main;

    client_max_body_size 4M; # 413 Request Entity Too Large

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # websocket headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_read_timeout 86400;
        proxy_set_header Origin "http://localhost:8080";
    }
}
```

nginxが80ポートで受け取って，8080番ポートに受け流す設定になってます．
JupyterHubはファイルのアップロード機能もついているので，アップロード可能のファイルサイズも`client_max_body_size`で指定しておきます．
この設定によって捌かれたリクエストは`/var/log/nginx/jupyterhub.log`に記録されるようになっています．出力フォーマットは`/etc/nginx.conf`に定義されてます．

# 動作確認
JupyterHubとnginxの設定が出来たら，両方とも起動して動作確認してみます．

## nginxの起動
まず，`sudo systemctl start nginx`で起動させます．
`Job for nginx.service failed because the control process exited with error code.`のような情報が出力されてしまったら設定ファイルに不備がある可能性があるので`systemctl status nginx`と入力して，原因を確認して修正していきます．
先ほど追加した`/etc/nginx/conf.d/jupyterhub.conf`にEC2インスタンスのパブリックDNS名を指定した場合，ドメイン名が長すぎて怒られてしまいます．
そのため，以下のように`server_names_hash_bucket_size`の項目を追加しておきます．
設定値は`64`だと小さいようなので`128`にしてあります．

```text:/etc/nginx/nginx.conf
... 省略 ...

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    server_names_hash_bucket_size  128;             # ★★ これ ★★

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

... 省略 ...

```

## JupyterHubの起動
nginxが起動したら，先ほどと同様にJupyterHubを起動します．
`http://{nginxで指定したサーバー名}/hub`とブラウザに打ち込んでアクセスしてみます．
以下のように表示されれば，設定完了です．
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165030/fda0dd58-6b61-840d-687e-e47d991ecd98.png)

`/var/log/nginx/jupyterhub.log`にもアクセスログが出力されています．
入力フォームに出ているワーニングは，「HTTP接続してんじゃねーよ」って言ってるだけです．あとで，ACMとALBを使ってHTTPS接続できるようにするので放置しておきます．

# まとめ
今回は，JupyterHubのインストールと設定，nginxとの接続の3つをやりました．
そして，JupyterHubのログイン画面を表示するところまで進めました．
今回の設定では，ホワイトリストに設定されているLinuxユーザであれば，ログインできるPAM認証というものを使っています．
今のままでは正しくユーザ名・パスワードを入力してもログインが失敗してしまいます．
次回，この失敗を解消させて，JupyterHubにログインできるようにしてきます．