CUBE SUGAR CONTAINER

技術系のこと書きます。

リモートサーバの Jupyter Notebook を SSH Port Forwarding 経由で使う

一般的に Jupyter Notebook はローカルの環境にインストールして使うことが多い。 ただ、ローカルの環境は計算資源が乏しい場合もある。 そんなとは IaaS などリモートにあるサーバで Jupyter Notebook を使いたい場面が存在する。 ただ、セキュリティのことを考えると Jupyter Notebook の Web UI をインターネットに晒したくはない。

そこで、今回は SSH Port Forwarding を使って Web UI をインターネットに晒すことなく使う方法について書く。 このやり方ならリモートサーバに SSH でログインしたユーザだけが Jupyter Notebook を使えるようになる。 また、Web UI との通信も SSH 経由になるので HTTP over SSL/TLS (HTTPS) を使わなくても盗聴のリスクを下げられる。

Jupyter Notebook をインストールする先の環境は次の通り。 話を単純にするために環境は Vagrant で作ってある。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ uname -r
4.15.0-29-generic

必要なパッケージをインストールする

ここからは、すでにリモートの Ubuntu マシンに SSH でログインしている前提で話を進める。

まずは必要なパッケージをインストールする。 ログインするたびに Jupyter Notebook を起動するコマンドを入力するのも面倒なので Supervisord でデーモン化することにした。

$ sudo apt-get update
$ sudo apt-get -y install jupyter-notebook supervisor

今回は OS のパッケージ管理システム経由でインストールしてるけど pip を使うとかはお好みで。

アクセス制御をかける

リモートサーバを想定しているので、念のため必要なポート以外はファイアウォールを使って閉じておく。

SSH に使うポートだけを残して、それ以外は全て閉じる。 SSH に使うポート番号を 22 以外にしているときは、適宜読み替える感じで。

$ sudo ufw allow 22
$ sudo ufw default DENY
$ yes | sudo ufw enable
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22                         ALLOW       Anywhere                  
22 (v6)                    ALLOW       Anywhere (v6)             

ファイアウォールの設定を変更するときはリモートサーバから追い出されないように注意しよう。

Jupyter Notebook を起動するユーザを追加する

若干好みの問題にも近いけど、念のため Jupyter Notebook を起動する専用のユーザを追加しておく。

$ sudo useradd -m -s $SHELL jupyter

Jupyter Notebook を設定する

ここからは Jupyter Notebook を設定していく。

まずは先ほど作ったユーザにログインする。

$ sudo su - jupyter

続いて、設定ファイルを生成する。

$ jupyter notebook --generate-config
Writing default config to: /home/jupyter/.jupyter/jupyter_notebook_config.py

Jupyter Notebook の作業ディレクトリを用意する。

$ mkdir -p /home/$(whoami)/jupyter-working

設定ファイルを編集する。

$ sed -i.back \
  -e "s:^#c.NotebookApp.token = .*$:c.NotebookApp.token = u'':" \
  -e "s:^#c.NotebookApp.ip = .*$:c.NotebookApp.ip = 'localhost':" \
  -e "s:^#c.NotebookApp.open_browser = .*$:c.NotebookApp.open_browser = False:" \
  -e "s:^#c.NotebookApp.notebook_dir = .*$:c.NotebookApp.notebook_dir = '/home/$(whoami)/jupyter-working':" \
  /home/$(whoami)/.jupyter/jupyter_notebook_config.py
$ cat ~/.jupyter/jupyter_notebook_config.py | sed -e "/^#/d" -e "/^$/d"
c.NotebookApp.ip = 'localhost'
c.NotebookApp.notebook_dir = '/home/jupyter/jupyter-working'
c.NotebookApp.open_browser = False
c.NotebookApp.token = u''

それぞれの設定の内容や意図としては以下のような感じ。

  • c.NotebookApp.ip = 'localhost'
    • Jupyter Notebook が Listen するアドレスをループバックアドレスにする
    • もしファイアウォールがなくてもインターネットからは Jupyter Notebook の WebUI に疎通がなくなる
  • c.NotebookApp.notebook_dir = '/home/jupyter/jupyter-working'
    • Jupyter Notebook の作業ディレクトリを専用ユーザのディレクトリにする
    • 仮に Web UI が不正アクセスを受けたときにも影響範囲を小さくとどめる (気休め程度)
  • c.NotebookApp.open_browser = False
    • 起動時にブラウザを開く動作を抑制する
    • ローカル環境ではないので起動するときにブラウザを起動する必要はない
  • c.NotebookApp.token = u''
    • Jupyter Notebook の Web UI にビルトインで備わっている認証を使わない
    • 認証は SSH によるログインで担保する場合の設定 (心配なときは後述する共通パスワードなどを設定する)

(オプション) Jupyter Notebook の Web UI に共通パスワードをかける

SSH のログイン以外にも認証をかけたいときは、例えばシンプルなものだと共通パスワードが設定できる。

Jupyter Notebook の Web UI に共通パスワードをかけるには jupyter notebook password コマンドを実行する。

$ jupyter notebook password
Enter password: 
Verify password: 
[NotebookPasswordApp] Wrote hashed password to /home/jupyter/.jupyter/jupyter_notebook_config.json

すると、ソルト付きの暗号化されたパスワードが設定ファイルとしてできる。

$ cat ~/.jupyter/jupyter_notebook_config.json 
{
  "NotebookApp": {
    "password": "sha1:217911554b0b:f2fa9cd9f336951c335bdaa06a6c16eb6286c192"
  }
}

上記のやり方だとハッシュのアルゴリズムが SHA1 固定っぽい。 もし、より頑丈なものが使いたいときは次のように Python のインタプリタ経由で生成する。

$ python3
Python 3.6.6 (default, Sep 12 2018, 18:26:19) 
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from notebook.auth import passwd
>>> passwd('jupyter-server-password', algorithm='sha512')
'sha512:d197670d2987:19bb2eedfc6fde56f1a9fc04d403999c3f03a99af368e528f45ee9a68f01a7c5f07e375bd34ec176d1c66a0f2e8ef7615ebcf9e524a23ace5ab6dd5a930398d4'

生成した暗号化済みパスワードを、次のような形で Jupyter Notebook の設定ファイルに入力すれば良い。

c.NotebookApp.password = u'sha512:d197670d2987:19bb2eedfc6fde56f1a9fc04d403999c3f03a99af368e528f45ee9a68f01a7c5f07e375bd34ec176d1c66a0f2e8ef7615ebcf9e524a23ace5ab6dd5a930398d4'

上記の共通パスワード方式を含む Jupyter Notebook の認証周りについては以下の公式ドキュメントを参照のこと。

Running a notebook server — Jupyter Notebook 5.7.0 documentation

Jupyter Notebook を Supervisord 経由で起動する

続いては Jupyter Notebook をデーモン化する設定に入る。

一旦、元の管理者権限をもったユーザに戻る。

$ exit
logout

Supervisord の設定ファイルを用意する。

$ cat << 'EOF' | sudo tee /etc/supervisor/conf.d/jupyter.conf > /dev/null
[program:jupyter]
command=jupyter notebook
user=jupyter
stdout_logfile=/var/log/supervisor/jupyter.log
redirect_stderr=true
autostart=true
autorestart=true
EOF

Supervisord を起動する。

$ sudo systemctl enable supervisor
$ sudo systemctl reload supervisor

ちゃんと Jupyter Notebook が起動しているかを確認する。

$ ps auxww | grep [j]upyter
jupyter   4689 27.0  5.4 183560 55088 ?        S    16:31   0:01 /usr/bin/python3 /usr/bin/jupyter-notebook
$ ss -tlnp | grep :8888
LISTEN   0         128               127.0.0.1:8888             0.0.0.0:*       
LISTEN   0         128                   [::1]:8888                [::]:*       

もし、上手く立ち上がっていないときはログから原因を調べよう。

$ sudo tail /var/log/supervisor/supervisord.log 
$ sudo tail /var/log/supervisor/jupyter.log 

SSH Port Forwarding 経由で Jupyter Notebook の Web UI にアクセスする

ここまでで、リモートサーバ上の Jupyter Notebook の設定は終わった。

一旦リモートサーバから SSH でログアウトする。

$ exit

改めて SSH Port Forwarding を有効にしてリモートサーバにログインする。 このときリモートサーバの TCP:8888 ポートを、ローカルホストのポートにマッピングする。 ユーザ名やホスト名は適宜読み替える。

$ ssh -L 8888:localhost:8888 <username>@<remotehost>

今回は Vagrant の環境を使っているので、手っ取り早くは以下のようにすると良い。 恒久的に対応するときは Vagrantfile を編集してポートフォワーディングの設定を入れる。

$ vagrant ssh-config > ssh.config
$ ssh -L 8888:localhost:8888 -F ssh.config default

あとは、ローカルマシンのブラウザでローカルホストにマッピングしたポート番号を開く。

$ open http://localhost:8888

すると、見覚えのある Web UI が表示される。

f:id:momijiame:20181014022447p:plain

あとは、もしポータビリティとかを考えるのであればお好みで Docker イメージとかにする感じで。

めでたしめでたし。