可能な限り沢山の実行環境を詰め込んだJupyter Notebook環境の紹介

これはQiitaアドベントカレンダー2019 Jupyter Notebookの8日目の記事です。

> Jupyer Kernelって一つのDockerイメージに何個まで詰め込めるのでしょうか？
> これってトリビアになりませんか？

## 今日のトリビア
Jupyter Kernelは一つのDockerイメージに??個まで詰め込める。

## 実験環境

実験は次のような環境で行なった。

|項目 |項目 |
|:-:|:-:|
|ホストOS| Ubuntu 18.04 |
|ホストCPU| 少し前のIntel Core i7 8コア|
|ホストメモリ| 16GB |
|コンテナ環境| Docker 19.03 (Rootless)
|ベースイメージ|Fedora|

## 実験方法

Dockerでカーネルを沢山繋ぐにはいくつの方法がある。

- `/.local/share/jupyter/kernels`配下にカーネルの設定ファイルを書く
- remote_ikernelを使ってssh経由でカーネルを起動し接続する

１番目の方法の利点は、Jupyterがデフォルトで提供している設定方法であり安定しているというところ。しかし、カーネルを沢山詰め込むとなるとカーネル同士の干渉が発生することがある。例えばAnaconda環境下であるカーネルをインストールすると、他のAnacondaでインストールしていないカーネルもAnaconda（正確にはConda-forge)が提供するリンカを利用して実行されることになり、Cのランタイムライブラリのバージョンが一致しないと実行に失敗することがある。これを避けるにはAnacondaをインストールしないことが解決策になるが、カーネルのインストールが若干煩雑になる。

２番目の方法の利点は、SSHで経由でカーネルを実行できるのでDocker-compose等でJupyter-kernelのクラスタを構成することができるという点がある。つまり、Jupyterはremote-ikernelが提供するカーネルを呼びremote-ikernel自身はプロキシーとなりsshで接続したJupyter Kernelに入力を渡すという形である。しかし、この方法はいくつかのカーネルでうまく動作しない。これは、古いJupyter KernelのAPIを参照していたりlocalhostを前提に設計されたKernelでKernelのシャットダウンがうまく行かないなどの問題がある。

そこで今回は**1番目**の方法を採用した。Anacondaは使わずFedoraが提供するパッケージやプログラミング言語のコミュニティが提供するパッケージマネージャ(npm, pip等)のみを使ってJupyter環境を構成した。

Jupyter Kernelが大量になるにつれてDockerイメージのビルド時間が長くなっていったのでこれに対処するためにマルチステージビルドを導入しカーネルごとにビルドを行い、最終的にカーネルの実行に必要な部分をメインのDockerイメージにコピーするという方針にした。結果的に初回ビルド時のコンパイル時間は変わらないが、2回目以降のビルドでビルド済みのカーネルはDockerfileの記述に独立してキャッシュされるようになり、大幅な時間短縮となった。

また、Jupyter Kernelとは関係ないがJupyter LabにはLaTeXを編集する拡張機能があるので、ついでにTeXLiveも追加してみた。

## 実験結果

最終的な成果物はこちら [jupyter-metropolis](https://github.com/fgborges/jupyter-metropolis)
結果として37のプログラミング言語のJupyterKernelを追加することに成功した。LaTeXの編集機能と加えると実に38のプログラミング言語を編集することが可能となっている。対応言語は以下の通りである。

```
[jovyan@1ca444c5adb0 ~]$ jupyter kernelspec list
Available kernels:
  kotlin              /home/jovyan/.ipython/kernels/kotlin
  .net-csharp         /home/jovyan/.local/share/jupyter/kernels/.net-csharp
  .net-fsharp         /home/jovyan/.local/share/jupyter/kernels/.net-fsharp
  agda                /home/jovyan/.local/share/jupyter/kernels/agda
  bash                /home/jovyan/.local/share/jupyter/kernels/bash
  c                   /home/jovyan/.local/share/jupyter/kernels/c
  clojupyter-0.2.3    /home/jovyan/.local/share/jupyter/kernels/clojupyter-0.2.3
  common-lisp         /home/jovyan/.local/share/jupyter/kernels/common-lisp
  coq                 /home/jovyan/.local/share/jupyter/kernels/coq
  fortran             /home/jovyan/.local/share/jupyter/kernels/fortran
  go                  /home/jovyan/.local/share/jupyter/kernels/go
  groovy              /home/jovyan/.local/share/jupyter/kernels/groovy
  haskell             /home/jovyan/.local/share/jupyter/kernels/haskell
  ielixir             /home/jovyan/.local/share/jupyter/kernels/ielixir
  iperl               /home/jovyan/.local/share/jupyter/kernels/iperl
  java                /home/jovyan/.local/share/jupyter/kernels/java
  jslab               /home/jovyan/.local/share/jupyter/kernels/jslab
  julia-1.2           /home/jovyan/.local/share/jupyter/kernels/julia-1.2
  lua                 /home/jovyan/.local/share/jupyter/kernels/lua
  ocaml               /home/jovyan/.local/share/jupyter/kernels/ocaml
  octave              /home/jovyan/.local/share/jupyter/kernels/octave
  pari_jupyter        /home/jovyan/.local/share/jupyter/kernels/pari_jupyter
  perl6               /home/jovyan/.local/share/jupyter/kernels/perl6
  powershell          /home/jovyan/.local/share/jupyter/kernels/powershell
  prolog              /home/jovyan/.local/share/jupyter/kernels/prolog
  racket              /home/jovyan/.local/share/jupyter/kernels/racket
  ruby                /home/jovyan/.local/share/jupyter/kernels/ruby
  rust                /home/jovyan/.local/share/jupyter/kernels/rust
  scala               /home/jovyan/.local/share/jupyter/kernels/scala
  singular            /home/jovyan/.local/share/jupyter/kernels/singular
  tslab               /home/jovyan/.local/share/jupyter/kernels/tslab
  vim_kernel          /home/jovyan/.local/share/jupyter/kernels/vim_kernel
  gap-4               /usr/share/jupyter/kernels/gap-4
  ir                  /usr/share/jupyter/kernels/ir
  python3             /usr/share/jupyter/kernels/python3
  python3-jupyroot    /usr/share/jupyter/kernels/python3-jupyroot
  sagemath            /usr/share/jupyter/kernels/sagemath
```

上記はカーネル名なので、改めてプログラミング言語を列挙すると、
Kotlin, C#, F#, Agda Bash, C, C++(jupyroot), Clojure, Common Lisp, Coq, Fortran, Go, Groovy, Haskell, Elixir, Perl, Java, JavaScript, Julia, Lua, OCaml, Octave, PARI, Raku(旧Perl6), PowerShell, Prolog, Racket, Ruby, Rust, Scala, Singular, TypeScript, VimScript, GAP, R, Python3, Sage, (LaTeX)
である。ほかにGaucheやGuileといったScheme系等のカーネルもあったが今回はビルド環境（特にGuileカーネルの方）を整えるのが億劫だったこと、厳密には同一ではないが下記のようにRacketの互換ライブラリで十分だったことから取り入れていない。

```
(import (rnrs lists (6))
        (rnrs base (6))
        (rnrs io simple (6))
```

合計で20GBほどの巨大なDockerイメージが完成した。自分のプライベートレジストリにあげるのが精一杯でGithub Package RegistryやDocker Hubにはアップロードすることができなかったので、各自でイメージをビルドしてローカルネットワーク上で共有するなどして使用するのが良いと思われる。

## まとめ

こうしてこの世界にまた一つ新たなトリビアが生まれた。
Jupyterカーネルは一つのDockerイメージに**37**個まで詰め込める。