Port Forwardingによるリモートで立ち上げたJupyterへの接続

# はじめに

最近，リモートサーバ上で立ち上げたJupyter NotebookをローカルのPCで操作できることを初めて伝え知りました．そこで初めてwebで調べてみると，そこそこ記事が散見されることがわかりました．ポートフォワーディングについてよくわかっていなかった部分もあり，ぱっと見た限りは接続の構築が非常に複雑に思えました．こうした経緯から，今回はポートフォワーディングの接続に関し，手を動かしながら詳細に理解してまとめることにしました．
前提として，既にリモート環境にはJupyterがインストールされていることとします．個人PCはWindows OSを想定します．その際，Puttyを使用して接続を行います．mac OSの場合は，図に示すsshコマンドを直接用いるのもよいですし，~/.ssh/configの設定なども同コマンドで調べれば簡単に出てきます．
また，今回の方法は，Jupyter Notebookの後継であるJupyterLabでも全く同様に機能します．

# Jupyter Notebookを使うメリット

昔からJupyter Notebookに触れずにPythonを使用してきた人々にとっては，Jupyterを使うメリットがあまりないと考えることもあるかと思います．個人的に考える使用メリットを挙げると次のようになります．

- **途中段階のデータの保持**

これはipythonとも共通しますが，主な用途の一つは，関数や構文などの動作確認にあると思います．一連の処理を一度に実行するスクリプト操作とは異なり，ひとつひとつ処理を終えていくため，途中段階のデータを確保することができます．何かのタイミングでエラーが出た場合，すぐにデバッグして処理を再開することが可能です．

- **テキストや画像などによる詳細な説明・ログを残すことが可能**

Jupyter Notebookにはコーディング用セルとMarkdown用セルがあり，コードと説明を同時に保持することができます．Markdown用セルには画像の挿入なども可能であり，webで調べて出てきた図をコピペしたりもします．また，ブラウザ上で立ち上げるために印刷やPDF化もでき，レイアウトも申し分ないと思います．個人的には，コードの真下に実行結果がずっと残ってくれる点が一番うれしい点かもしれません．時間がたった後に作業を復習する際のドキュメントはわかりやすいに越したことはありません．


- **まるっと配布が可能**

他者に知識を共有するのを容易にするため，教育の観点で非常に優れていると思います．また，新たなパッケージを導入する際に，パッケージ提供元がその使用例をJupyter Notebookによって説明していることも多いです．その場合は，ハンズオンで非常に短時間に多くの情報を得ることができます．

- **ショートカットや補助機能が充実している**

コーディング用に開発されたテキストエディタや統合開発環境には劣るかもしれませんが，キーボード操作で事足りるためのショートカットも充実しています．また，レイアウトや表示機能を追加・変更するための様々な補助機能が開発されており，自分好みの作業環境をつくることも可能です．


## サーバ上で立ち上げるメリット

この方法を知る前は，個人PCのPython環境でJupyter Notebookを起動していました．しかし，やはりサーバ内にある情報をわざわざ手元にダウンロードしたり，普段使用しているサーバ内のpython環境と同じパッケージをインストールしたりなど，いろいろ手間がかかります．これから説明する方法でリモート環境でJupyter Notebookを立ち上げられるようになってからはこうした不満はなくなりました．また，サーバ上の計算能力を普段通り使用できる点も大きいでしょう，

# ポートフォワーディング（Port Forwarding）

話が長くなりましたがここから接続の説明です．通常のssh接続では，双方向に送受信が可能な状態（ESTABLISHED）になります．それに対し，ポートフォワーディングでは，片側のPCがもう片側からのデータ転送を待機する状態（LISTEN）を作ります．

![Qiita_tcp0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187118/829119db-5f1a-3761-ecbf-87a07348fa7d.png)

**※図中のコマンドのオプションは最小限のものを表示しています（以後同様）．**

上図の2つのコマンドは，実行するマシンが異なるだけで，やっていることは全く同じです．いずれも，赤破線で示す通り「PC AのポートXXXXにアクセスすることで，PC BのポートYYYYで起動しているアプリを覗く」ことを可能とさせます．PC Aにとって**自分はLocalなのでLオプション**を，PC Bにとって**自分はRemoteなのでRオプション**を使用します．ポート番号指定部分は**ListenのFromToの関係**で一致しています（データ転送の向きとは逆）．この辺りを頭に入れておけば，混同することはなくなるかと思います．

今回は，各ポート番号の関係が分かりやすいように，各マシンごとに異なるポート番号を割り当てることにしました．基本的に8000以上のポート番号のほとんどは使用されていません．
既に使用されている場合は，接続時に下記のようなエラーメッセージが出ると思います．

```
Warning: Remote port forwarding failed for listen port XXXX
```

この場合は，ポート番号を変更して再度実行する必要があります．以下の説明でもその点に触れていきます．

# 全体の流れ

さて，今回は，次の4ステップを実現することを目指します．

1. **個人PCのポート8008から，踏み台サーバ（Bserver）のポート8088を覗けるようにする**
2. **踏み台サーバ（Bserver）のポート8088から計算用サーバ（Rserver）のポート8888を覗けるようにする**
3. **計算用サーバ（Rserver）のポート8888でJupyter Notebookを起動する**
4. **個人PCのブラウザでlocalhost:8008にアクセスしてNotebookを利用する**


ここで，2ステップ目をどのように実現するかで2通りの接続方法があるので，それぞれ説明していきます．

# 接続方法1

![Qiita_tcp1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187118/8519fd4f-6248-75e2-5b82-3afcd609c175.png)

## 1-1 個人PC→踏み台サーバ（Bserver）

![Qiita_putty1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187118/e7466cb2-2529-b49a-4a7e-a6a5a7fb3958.png)

ホスト名はにBserverを指定します．右のportは，図中で黒矢印で示した通常のSSH接続に用いる（ポートフォワーディングとは異なる）ものなので，デフォルトの22のままで問題ありません（もしくはコミュニティのルールに従ってください）．右図のツリー位置にあるトンネル（Tunnels）で，ポートフォワーディングの設定を行います．
まず，接続先の真下のLocalボタンが黒くなっていることを確認します．ソースに，個人PCのポート（8008）を，接続先に，Bserverのlocalhost:8088を入力して，すぐ上のAddボタンを押すと設定が登録されます．

## 1-2 踏み台サーバ（Bserver）→計算用サーバ（Rserver）

<img width="339" alt="Qiita_putty2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187118/36249aa9-58bd-70fc-61d3-23d0780b272c.png">

Puttyを用いて，Bserverを飛び越えてRserverに飛ぶ際は，ツリーのSSHにあるリモートコマンド欄を入力します．ここに入力したコマンドはBserverへログイン時に実行されます．ポートフォワーディングが不要な場合はssh Rserver.xxx.yyyのみで十分です．今回は，BserverからRserverへもフォワーディングしたいので，Lオプションにて設定を加えます．コマンドでは省略していますが，”-p 22”のようにSSH接続用のポート設定もできます．デフォルトは22ですが，やはりサーバ利用のルールに従って設定してください．

## 1-3 計算用サーバ（Rserver）でJupyter Notebookを起動

上記の設定でRserverにログインした状態で（作業シェルは別でも同じでも大丈夫），作業用ディレクトリに移ります．必要があればpythonの仮想環境を立ち上げ，次のコマンドを実行します．

```
jupyter notebook --no-browser --port 8888

# ↓↓↓ JupyterLabの場合 ↓↓↓
jupyter lab --no-browser --port 8888
```

だらだらといろいろな出力が出てきたのち，最後に次のようなアドレスが出てきます．

```
http://localhost:8888/?token=xxxxxxxxxxxxxxxxxxxxxxx...
```

仮にここで，8888以外のポートが指定されている場合，jupyterコマンドが使用済みのポートを避けて勝手に割り当てたものになります．この場合，Rserverのポート番号を変更して，ポートフォワーディングからやり直してください．


## 1-4 個人PCのブラウザでlocalhost:8008にアクセス

tokenの入力画面が出てきたら無事接続成功です．
仮に全く別の画面が出てきた際は，これもBserverで既に開かれているアプリによるものと考えられますので，Bserverのポートを変更してください．

# 接続方法2

![Qiita_putty2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187118/dbe2dc72-80a8-cca0-edf6-75e34cd3692c.png)

## 2-1 個人PC→踏み台サーバ（Bserver）

接続方法1と全く同様です．

## 2-2 踏み台サーバ（Bserver）→計算用サーバ（Rserver）

先ほどはLオプションを加えたSSHのリモートコマンド欄ですが，今回はssh Rserver.xxx.yyyのみでログインします（以降の作業シェルはこれと別でも同じでも大丈夫）．ここで，Rserverのほうからポートフォワーディングを行います．先ほどとはコマンド実行場所が逆転したので，今回はRオプションです．

```:（未完成）そのままログインしてBserverを起動してしまう
ssh -R 8088:localhost:8888 Bserver.xxx.yyy
(Password:xxxxxxxx)
```

上記コマンドでポートフォワーディングは成功しますが，Bserverに再度ログインしてしまうことになります．できればポートフォワーディングだけしてセッションはRserverのままでいたいものです．そこで，オプションを加えます．

```:（未完成）シェル起動はしないがセッションに戻れない
ssh -N -R 8088:localhost:8888 Bserver.xxx.yyy
```

Nオプションは，直後のコマンド入力が無効になるオプションです．
これによって，Bserverでシェルが起動することがなくなりましたが，Rserverのフォアグラウンドで停止した状態になります．

```:（ほぼ完成）ポートフォワーディングをバックグラウンドで実行
ssh -f -N -R 8088:localhost:8888 Bserver.xxx.yyy
```

さらにfオプションを追加することによって，ポートフォワーディングをバックグラウンドに移してくれます．
最後に，ssh接続時にBserverにて警告文が表示されないようにオプションを追加します（[参照リンク](https://qiita.com/shotaTsuge/items/48bdaccdafa5475d9016)）．

```:（完成）ポートフォワーディングをバックグラウンドで実行・警告文無視
ssh -o StrictHostKeyChecking=no -f -N -R 8088:localhost:8888 Bserver.xxx.yyy
```

Rserver→Bserverの通常SSH接続のポート番号に関してルールがある場合，-pオプションを追加してください．
そのままRserverの同じシェルで作業を続行します．


## 2-3 計算用サーバ（Rserver）でJupyter Notebookを起動

接続方法1と同様，ディレクトリへの移動，python立ち上げ，そしてjupyterの起動を行います．起動時の注意事項も同様です．

## 2-4 個人PCのブラウザでlocalhost:8008にアクセス

接続方法1と全く同様です．
 
## 2-X 計算用サーバ内でのエイリアス化

接続方法2では，複数の長いコマンドを実行することになるので，エイリアス化するのがよいでしょう．下記のコードをユーザーのホーム下の.bash_profileか.bashrcのいずれか（ログイン時に自動的に実行されるコマンドファイル）に追記すれば，jpnコマンド一つ打つだけでポートフォワーディングとjupyterの起動を一括でできます．
ポート番号を固定しているため，他のアプリに邪魔されないことを確認して使ってください．

```bash:接続方法2のエイリアス
#!/bin/bash
function jpn() {
	# ↓↓↓補足1↓↓↓
	unset XDG_RUNTIME_DIR 
	ssh -o StrictHostKeyChecking=no -f -N -p 22 -R 8088:localhost:8888 Bserver.xxx.yyy
	jupyter notebook --no-browser --port 8888 
	# ↓↓↓補足2↓↓↓
	pgrep -u $USER -x ssh -a | grep 8088:localhost:8888 | cut -d' ' -f 1 | xargs kill
}
alias jpn='jpn'
```

**補足1**：[このリンク](https://qiita.com/jizo/items/5449c3be3fa18205fbf0)にあるようなエラー対策です．
**補足2**：jupyterを閉じた際，バックグラウンドのポートフォワーディングを切断するコマンドです．プロセスIDを特定するために少し長くなってしまっています．．．

# ポートフォワーディングがなされていることの確認

ポートフォワーディングの状態を確認したい場合，または失敗する場合，下記のようなコマンドを，Listenする側される側の両方のサーバで試してみるとよいかと思います．

```
# "list open files" 指定したポートで開いているファイルやステータスの詳細（su権限で実行）
sudo lsof -i:XXXX

# "process g/re/p" ポート番号を含む実行中コマンド情報を表示（su権限不要） 
pgrep -a | grep XXXX
```

# その他

jupyterをバックグラウンドで走らせたり，個人用config設定をしたりと，まだまだできることはありそうです（[参考リンク](https://qiita.com/ishizakiiii/items/b98bbf8997f039f40058)）．

# おわりに

今回は2通りの接続方法を説明しましたが，個人的には後者の方を使っています．理由は，コマンド上でRserverでのポート番号の指定が容易だからです（上記エイリアスの部分で，PORT=$1という変数を導入しています）．できれば一度保存したPuttyの設定はいじりたくないので，こっちのほうがいいです．
個人的には便利だと思うことでも，他者に伝えようとするとその良さが伝わりづらいものです．本記事で少しでも読者の知識や選択肢の足しになればと思います．