GCE の docker で jupyter notebook を動かしてローカルブラウザで使うには

今回、わけあって GCE の docker で jupyter notebook を動かして、それを手元のマシンのブラウザから使う機会があり、その設定にちょっと、いやかなり手こずったので、備忘録を残します。2023/2/28の情報です。

なお、下に記しますが、ある方が残してくださった情報で非常に助かったので、その恩返しも込めて、冗長ではありますがGCEのインスタンスを立てるところからすべて記録しておきます。

ということで、まずはGCEでまっさらのインスタンスを立てます。名前は適当に、今回は「notebook-test」にします。OSは今回はDebianにしましたが、Ubuntuでもfedoraでもだいたい同じだと思います。なお、GCPのプロジェクトは何か自由に使えるものがあることは前提です。

適当にノートブックを立てる
OSも適当に。ディスクサイズは10だとDockerが動かないので、100くらい。jupyter notebookを動かす docker コンテナはひとつでだいたい3GB~7GB程度。
HTTPもHTTPSも絶対にONしません。おそろしい。

作ったインスタンスにログインして、Dockerをインストールします。公式サイト https://docs.docker.com/engine/install/ より、Docker Server をインストール。さきほど選んだOSのページ(例えばDebianだと https://docs.docker.com/engine/install/debian/ )に行くとインストール方法がまるっと書いてあるので、順に実施。

docker run hello-world がうまくいったので OK

続いて、jupyter notebook の docker image を 持ってくる。docker hub  https://hub.docker.com/ で jupyter notebookを検索すると、Verified Content にそれっぽいモノがたくさんあるので、今回は scipy-notebook を選んでみる。 pull command をコピーして実行。(ここからはrootではなく一般ユーザーでやったのですが、docker コマンドはすべて sudo で実行するのが一般的のようです。rootでも同じようにできるかと。)

$ sudo docker pull jupyter/scipy-notebook


scypi-notebook のイメージがやってきました。

では、docker run にかかりますが、実はこのまま

$ sudo docker run -it jupyter/scipy-notebook

を実行しても、GCEインスタンス内の、さらに docker コンテナの中で動き始めた jupyter notebook にアクセスする方法がありません。立ち上げたときのメッセージには、 「http://120.0.0.1:8888/lab にアクセスせよ」、と出るのですが、dockerコンテナはウェブブラウザを持っていないので、見られません。

http://127.0.0.1:8888/lab にアクセスせよと言われても、ブラウザを持っていない。

試しに dockerコンテナに入って、curl でアクセスしてみると(そもそもcurl をインストールするところから)、

コンテナ内からcurlで指定のURL:portをたたくと、応答がある。(何も無いけれど、本当に無いところは Connection refused が返ってくるので、ある。)

ですので、docker run 実行時にコンテナのポートを、docker を動かしているマシンのポートに転送してあげないといけない。さらには、今 docker を動かしているのはGCEのインスタンスで、これもグラフィカルなウェブブラウザを使えないので、じゃあどうやって jupyter notebook に繋ぐかというと、cloud shell の「ウェブでプレビュー」を使います。以下、そのための準備をします。

だいたいの流れ。notebook のポート8888をGCEインスタンスの8881へ、さらにそれをsshポート転送でcloud shellの8080へ飛ばして、そこへブラウザからアクセスする。

まず、docker run をする際、コンテナの8888ポート(notebookはデフォルトでこのポートを使う)をGCEインスタンス127.0.0.1の8881ポートにバインドするため、「-p 127.0.0.1:8881:8888」のオプションを付けます。また、もう一つあとで困らないためのおまじないをつけて、

$ sudo docker run -p 127.0.0.1:8881:8888 --name mybook jupyter/scipy-notebook start-notebook.sh --NotebookApp.allow_origin="*" --NotebookApp.token=""

とします。これで、上の図の8881まで繋がりました。おまじないの所はあとで説明します。

次は、GCEインスタンスの8881と cloud shell の8080を繋ぎます。GCP の cloud shell から gcloud compute ssh で、上で動かしているGCEのインスタンスに繋ぐのですが、

$ gcloud compute ssh notebook-test --project <proj-name> --zone <zone-name> --ssh-flag="-L" --ssh-flag="localhost:8080:localhost:8881"

インスタンス名は一番上でつけた「notebook-test」で、プロジェクト名とzone名は適宜使っているモノを入れてください。で、最後に sshのポート転送のオプションを記入します。この例では GCEのインスタンスの8881を、cloud shell の8080に繋いでいます。

最後に、Cloud Shell の右上のボタンから「ウェブでプレビュー」を押し、「ポート8080でプレビュー」を選択すれば、めでたく jupyter notebook が現れました。

右上の「ウェブでプレビュー」を押す。青い丸になっているボタンの右隣。
ポート8080でプレビュー

ローカルマシンのchromeで繋がった jupyter notebook
ipykernelも無事起動し、計算もできました。

これで終了ですが、最後にいくつか注記。

まず、なぜこんな紛らわしい方法を使ってまで、GCEを使うのか。それは、データのセキュリティです。インターネット黎明期の20年前は、データは物理的に手元に置いて、厳重に鍵を掛けるのがセキュリティでしたが、今は違います。全ての機器がネットワークに繋がって、そうしないと何の仕事もできない時代になりました。(ネットワーク経由でアプデできないソフトウェアなど怖くて使えません。)そうすると、今はローカルマシンにデータがあるよりクラウドにあった方が安全です。なぜなら、日々いたちごっこが続くインターネット上の脅威に一番敏感で、一番勉強していて、一番手を動かしているのがクラウド事業者だからです。その中でも Google、Azure、AWS などの大手事業者は、世界中から超優秀な人材をかき集めてセキュリティ対策をしています。そんなのに自前のセキュリティ対策が勝てるわけがないので、餅は餅屋、セキュリティで一番信頼できるのはクラウド事業者なので、データはクラウドにあった方が安全。使う側は、使い方を間違わないようにすれば良いです。
ということで、データはクラウドから出したくない。しかし、jupyter notebook のようなグラフィカルインタフェースを持ったツールは使いたい。そこで、普段は Vertex AI Workbench や azure ML studio のような、それ自体のサービスを使っていますが、今回たまたま docker を使う必要があった(とあるイメージを使って構築する必要があった)ので、というわけです。

さて、そういう理由なので、せっかくセキュアなGCPのプロジェクト内で立ち上げたGCEインスタンスに、httpやhttpsの穴を開けるというのは本末転倒。「HTTPトラフィックを許可」のチェックは絶対にONにしたくありません。あれはちゃんとインターネットセキュリティのことを熟知したSIerなら自身の責任の下にONできますが、素人が触っていいチェックボックスでは無いです。ですから、それ以外の方法で、安全に、GCEインスタンスにブラウザからアクセスできる、「ウェブでプレビュー」はめちゃめちゃ嬉しい仕組みです。

それを踏まえて、docker run 時のおまじないについて。(実は、このおまじないが見つかるまで困っていたのですが、https://azechi-n.hatenadiary.com/entry/2019/11/21/135158 こちらの記事に助けてもらいました。ありがたい。)つけたおまじないは二つ、

$ sudo docker run -p 127.0.0.1:8881:8888 --name mybook jupyter/scipy-notebook start-notebook.sh --NotebookApp.allow_origin="*" --NotebookApp.token=""

「--name mybook」は単にコンテナに名前をつけただけで、ポイントは「 start-notebook.sh --NotebookApp.allow_origin="*" --NotebookApp.token=""」。まず allow_origin は、この notebook にアクセスするブラウザの「怪しさ」を制御するものです。jupyter notebook にはセキュリティの一環として、アクセスしてくるブラウザがアクセスしている origin URL とHostが異なるとエラーにする、という仕組みがあるようで、allowしていないと「Blocking Cross Origin API request」というエラーが出ます。今回の仕組みでは、ブラウザは cloud shell が用意した URL にアクセスしているのに、host名は 127.0.0.1 なので、おかしいじゃないかとはじかれます。ですので、allow_origin をワイルドカードにして、originがなんでも許すよ、と設定します。また、次の「token=""」はアクセスする際のトークン認証を不要にするオプションで、これもjupyter notebook のセキュリティを落とします。これが無いと token認証が行われるのですが、やはり origin と host が違うことによるものなのかなんなのか(実はここの仕組みはよくわかっていません)、token認証画面から先に進めなくなります。

token="" がなくても、ここまではたどり着く。そこで token を入力すると
左上にちっちゃくjupyterのマークがあるだけの真っ白な画面のまま進まない。

tokenの問題は何かもっと理解していればちゃんと対処できるような気がするのですが、ひとまずはこれでも可と思うのは、上で書いたように、そもそもこのネットワーク全体が googleさんに守ってもらっているからです。

この docker が動いているインスタンスは GCE にあって、かつ、外にHTTPやHTTPSのポートを開いていません。よって、外からは誰もこのサーバーにはアクセスできません。GCPのプロジェクト内からはアクセスできますが、それはプロジェクトのセキュリティを保っておけば十分です。少なくとも今は自分以外に誰もアクセスできません。また、cloud shellからの接続も、これは私のGoogle認証を使って SSL で cloud shellに繋いでいるので、自分以外はアクセスできません。よって、jupyter notebook の allow-origin や token認証をなくしてしまっても問題ないと考えます。(たぶん大丈夫だと思いますが、もしどこかに穴があったら教えてくださるとありがたい。)

ということで、めでたく GCE のインスタンスの中の docker コンテナの上で動かした jupyter notebook を、ローカルマシンのブラウザで使えるようになりました。

この記事が気に入ったらサポートをしてみませんか?