JupyterHubをKubernetes(Amazon EKS)にインストール(zero-to-jupyterhub-k8s 0.9.0)

Amazon EKSにJupyterHub zero-to-jupyterhub-k8s(Z2JH)でインストールする方法をまとめます。

github.com

以下の手順に従って作業を実施します。

Zero to JupyterHub with Kubernetes — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

Kubernetesのセットアップ

EC2をワーカーノードとしたEKSクラスタを作成します。Fargateへの導入も試しましたが、執筆時点(2020/11/27)では後述の通り未対応のようです。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: <cluster-name>
  region: ap-northeast-1
  version: "1.18"

managedNodeGroups:
  - name: <node-groupname>
    ...
$ eksctl create cluster -f cluster.yaml

Helmのセットアップ

Helmをインストールします。

$ curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 11213  100 11213    0     0  38010      0 --:--:-- --:--:-- --:--:-- 38010
Downloading https://get.helm.sh/helm-v3.4.1-darwin-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
Password:
helm installed into /usr/local/bin/helm

以下のコマンドを実行し、空のリストが表示されればインストール成功です。

$ helm list
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION

JupyterHub のセットアップ

設定ファイルの準備

セキュリティトークproxy.secretToken を設定した設定ファイル config.yaml を作成します。
config.yaml はHelmのValuesファイルです。

$ cat << EOF > config.yaml
proxy:
  secretToken: "$(openssl rand -hex 32)"
EOF

JupyterHubのインストール

JupyterHubのHelmチャートリポジトリを追加します。

$ helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
"jupyterhub" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jupyterhub" chart repository
Update Complete. ⎈Happy Helming!⎈

config.yaml で設定したチャートをインストールします。
$RELEASE にはHelmのReleaseを、 $NAMESPACE にはKubernetesNamespaceを設定しています。

$ RELEASE=jhub
$ NAMESPACE=jhub

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

Release "jhub" does not exist. Installing it now.

Podの起動を確認します。

$ kubectl get pods -n jhub
NAME                              READY   STATUS    RESTARTS   AGE
continuous-image-puller-7hwqt     1/1     Running   0          2m6s
continuous-image-puller-7p8ln     1/1     Running   0          2m6s
continuous-image-puller-86c7j     1/1     Running   0          2m6s
hub-85f5d4b66b-nsk6q              1/1     Running   0          2m6s
proxy-694ff48877-l5dph            1/1     Running   0          2m6s
user-scheduler-56557c648d-sfh7f   1/1     Running   0          2m6s
user-scheduler-56557c648d-t5hb8   1/1     Running   0          2m6s

Service proxy-publicLoadBalancerとして作成されています。

$ kubectl get svc proxy-public -n jhub
NAME           TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)                      AGE
proxy-public   LoadBalancer   ...   ...   443:31337/TCP,80:30630/TCP   3h25m

公式の手順通りであればここで proxy-publicEXTERNAL-IP に接続できますが、 バージョン 0.9.0 だと以下のIssueによりロードバランサが落ちるため接続できません。
デフォルトでHTTPSが有効( proxy.https.enabled=true )となったことで、HTTPSが未設定の場合にヘルスチェックで落ちるようです。

github.com

HTTPS対応は後述するACMの証明書を使用したSSLの設定で対応するとして、一旦 https.enabledfalse を設定することで問題を回避します。

proxy:
  secretToken: "..."
  https:
    enabled: false

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

これで EXTERNAL-IP に接続できるようになります。
初期設定のAuthenticatorDummy Authenticatorなので好きなユーザでログインできます。

f:id:hiroki-sawano:20201125155418p:plain

ログインするとJupyter Notebookイメージを使用したシングルユーザJupyterノートブックサーバがPod jupyter-<username> (以降、ユーザPod)で起動し、ストレージ(PersistentVolume/PersistentVolumeClaim)が割り当てられます。

f:id:hiroki-sawano:20201127132034p:plain

以下はユーザ rick でログインした場合に作成されるリソースの例です。

$ kubectl get pods -n jhub
NAME                              READY   STATUS    RESTARTS   AGE
...
jupyter-rick                      1/1     Running   0          3m9s

$ kubectl get pv -n jhub
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                        STORAGECLASS   REASON   AGE
...
pvc-c60b01b1-7f42-44a6-8662-f54631bc5a52   10Gi       RWO            Delete           Bound    jhub/claim-rick              gp2                     21h

$ kubectl get pvc -n jhub
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
...
claim-rick      Bound    pvc-c60b01b1-7f42-44a6-8662-f54631bc5a52   10Gi       RWO            gp2            21h

ユーザPodは画面右上の QuitControl Panel->Stop My Server で削除されますが、 JupyterHubは一定時間ユーザによるアクションがない非アクティブなユーザPodを自動的に削除し、リソースを開放してくれます。
この挙動は config.yamlcull で変更可能です。

Customizing User Management — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

PVとPVCは明確に削除しない限りはユーザがサーバをシャットダウンしても残り続けます。
標準ではEBS(General Purpose SSD)にデータが保存されますが config.yamlsingleuser.storage で変更可能です。

Customizing User Storage — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

ACMの証明書を使用したSSLの設定

JupyterHubのインストールではHTTPで接続しましたが、より安全に通信するためにACMで管理する証明書を使用してSSLの設定を行います。
config.yamlproxy.httpsproxy.service の設定を追記します。
proxy.https.hosts には接続に使用するホスト名を、 proxy.service.annotationsservice.beta.kubernetes.io/aws-load-balancer-ssl-cert には証明書のARNを指定してください。

Security — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

proxy:
  secretToken: "..."
  https:
    enabled: true
    type: offload
    hosts: <hostname>
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "<arn>"
      service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"
      service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: '3600'

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

続けてRoute53でエイリアスレコードを作成し、 proxy.https.hosts に設定したホスト名で proxy-public (CLB)にトラフィックを流します。

f:id:hiroki-sawano:20201125170534p:plain

これでSSL通信に対応することができました。

f:id:hiroki-sawano:20201126185036p:plain

Jupyter Notebookイメージの変更

ユーザが使用するJupyter NotebookのDockerイメージは singleuser.image で指定することができます。

Customizing User Environment — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

Jupyter Docker Stacksから目的にあったイメージを選択します。
以下はJulia、Python、Rのデータ分析ライブラリが詰まったdatascience-notebookを使用する例です。

singleuser:
  image:
    name: jupyter/datascience-notebook
    tag: 177037d09156

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

Nodeにアクセスし、期待通りのイメージがPullされていることを確認できました。

$ ssh -i /path/to/key ec2-user@<node-external-ip>
[ec2-user@ip-... ~]$ docker images jupyter/datascience-notebook
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
jupyter/datascience-notebook   177037d09156        fb0e8e0b07c7        2 years ago         6.11GB

これでJulia、Python、Rのノートブックが作成できるようになりました。

f:id:hiroki-sawano:20201125181509p:plain

Adminユーザの設定

ユーザのサーバを管理するAdminユーザは auth.admin.users で設定できます。

Customizing User Management — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

auth:
  admin:
    users:
      - adminuser1
      - adminuser2

Adminユーザは Control Panel->Admin から各ユーザのサーバを起動・停止したり、ユーザのノートブックにアクセスすることができます。

f:id:hiroki-sawano:20201127140903p:plain

Authenticatorの変更

標準のDummy Authenticatorを適切なAuthenticatorに変更します。
以下のリンク先を参考にセットアップすることで、GitHubGoogle等のユーザでログインすることができます。

Authentication — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

標準でサポートしていないAuthenticatorを使用する場合は、後述するHubイメージの変更で使用したいAuthenticatorをJupyterHubにインストールし、任意の設定を適用(jupyterhub_config.pyの設定)c.JupyterHub.authenticator_classを設定します。

Hubイメージの変更

Hubイメージをカスタマイズしたい場合には以下の手順を実行します。

  • jupyterhub/k8s-hub:0.9.0 をベースとしたDockerfileを作成(※タグはHelmチャートのバージョンによって異なる)
  • 作成したイメージをDockerレジストリ(ECR)にPush
  • config.yamlhub.image でECRのレポジトリとタグを指定

ここでは例として、REMOTE_USERでユーザを認証するREMOTE_USER authenticatorをHubイメージに追加してみます。

FROM jupyterhub/k8s-hub:0.9.0
RUN pip install jhub_remote_user_authenticator

ビルドしたコンテナイメージをECRにPushします。名前やタグは任意ですがベースイメージと合わせました。

f:id:hiroki-sawano:20201125201459p:plain

config.yaml を編集し、ECRに登録したHubイメージを指定します。

hub:
  image:
    name: ...ecr.ap-northeast-1.amazonaws.com/k8s-hub
    tag: 0.9.0

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

起動したHubのPodに接続し、インストールの成功を確認します。

$ kubectl exec -it <hub-pod> -n jhub -- bash
jovyan@hub-...:/srv/jupyterhub$ pip freeze | grep authenticator
jhub-remote-user-authenticator==0.1.0
...

任意の設定を適用(jupyterhub_config.pyの設定)

Helmチャートで用意された設定では対応できない場合、 hub.extraConfig でJupyterHubの設定ファイル( jupyterhub_config.py )に任意のコードを追加することができます。

Advanced Topics — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

前述のHubイメージの変更で導入したREMOTE_USER authenticatorを使用してみます。

hub:
  extraConfig:
    authConfig: |
      c.JupyterHub.authenticator_class = 'jhub_remote_user_authenticator.remote_user_auth.RemoteUserAuthenticator'

Pod起動時のログを確認し、 Using Authenticator: ...RemoteUserAuthenticator と表示されていることが確認できました。

$ stern hub -n jhub
+ hub-c88c447c4-9c6t2 › hub
hub-c88c447c4-9c6t2 hub Loading /etc/jupyterhub/config/values.yaml
hub-c88c447c4-9c6t2 hub Loading /etc/jupyterhub/secret/values.yaml
hub-c88c447c4-9c6t2 hub Loading extra config: authConfig
hub-c88c447c4-9c6t2 hub [I 2020-11-25 11:32:20.104 JupyterHub app:2240] Running JupyterHub version 1.1.0
hub-c88c447c4-9c6t2 hub [I 2020-11-25 11:32:20.105 JupyterHub app:2271] Using Authenticator: jhub_remote_user_authenticator.remote_user_auth.RemoteUserAuthenticator
...

実際にHubにアクセスしてみると、REMOTE_USERがHTTPヘッダーにないため 401 : Unauthorized エラーとなります。

f:id:hiroki-sawano:20201125203433p:plain

Chrome拡張 ModHeaderでREMOTE_USERを設定するとログインに成功します。

f:id:hiroki-sawano:20201125204723p:plain

f:id:hiroki-sawano:20201125204913p:plain

おまけ:EKS on Fargateは未サポート

おまけです。今回ご紹介したHelmチャートはFargateに対応していませんが、インストールに挑戦してみた結果を残しておきます。
確認には以下のエントリで構築したEKSクラスタを使用しました。

hiroki-sawano.hatenablog.com

JupyterHubのインストールと同様に、以下のコマンドでチャートをインストールします(Namespaceはdefaultを指定)。

$ RELEASE=jhub
$ NAMESPACE=default

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

チャートのインストールがいつまで経っても終わらず、タイムアウトしてしまいました。

Error: failed pre-install: timed out waiting for the condition
$ helm list
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS  CHART               APP VERSION
jhub    default     1           2020-11-23 20:19:41.511629 +0900 JST    failed  jupyterhub-0.9.0    1.1.0   

Podを確認すると hook-image-awaiter-xxxxx がエラー終了しています。

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
hook-image-awaiter-52k6w          0/1     Pending   0          3s
hook-image-awaiter-ksxb5          0/1     Error     0          3m49s
hook-image-awaiter-p6tx4          0/1     Error     0          7m31s

エラーログは以下の通りです。

$ kubectl logs hook-image-awaiter-p6tx4
2020/11/23 11:23:28 Get https://10.100.0.1:443/apis/apps/v1/namespaces/default/daemonsets/hook-image-puller: dial tcp 10.100.0.1:443: getsockopt: connection timed out

hook-image-puller にFargateがサポートしていないDaemonSetを使用しているためだと思われます。
hook-image-puller は事前にDockerイメージを各NodeにPullしてくれるリソースです。

With the hook-image-puller enabled (the default), the user images being introduced will be pulled to the nodes before the hub pod is updated to utilize the new image.

関連するIssueも見つかりました。特に問題は解決しないままクローズされています。

github.com

ユーザPod起動時にイメージをPullする時間を許容できると仮定し、 DaemonSetを使わないようにhook-image-puller を無効化してみます。
同様にノード追加時にイメージをプルする continuous-image-puller も無効化します。

Optimizations — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

proxy:
  secretToken: "..."
prePuller:
  hook:
    enabled: false
  continuous:
    enabled: false

作成したリソースを削除し、再度 helm upgrade を実行します。
コマンドの実行が完了し、一見うまくいったように見えます。

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

Release "jhub" does not exist. Installing it now.
NAME: jhub
LAST DEPLOYED: Mon Nov 23 21:47:05 2020
NAMESPACE: jhub
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing JupyterHub!

Your release is named jhub and installed into the namespace jhub.

You can find if the hub and proxy is ready by doing:

 kubectl --namespace=jhub get pod

and watching for both those pods to be in status 'Running'.

You can find the public IP of the JupyterHub by doing:

 kubectl --namespace=jhub get svc proxy-public

It might take a few minutes for it to appear!

Note that this is still an alpha release! If you have questions, feel free to
  1. Read the guide at https://z2jh.jupyter.org
  2. Chat with us at https://gitter.im/jupyterhub/jupyterhub
  3. File issues at https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues

しかし、HubのPod( hub-xxxx )がPendingのままで起動することはありませんでした。

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
hub-5787d5cbc7-lnhsg              0/1     Pending   0          4m2s
proxy-694ff48877-8l7zl            1/1     Running   0          4m2s
user-scheduler-5f6c6d896d-8cnqh   1/1     Running   0          4m2s
user-scheduler-5f6c6d896d-l28mf   1/1     Running   0          4m2s

ここで大人しくEC2を使用する方針に切り替えたのでこれ以上の調査は行っていません。
正式にサポートされた際には再度確認したいと思います。

さいごに

本エントリではZ2JHを使用してJupyterHubをEKSに導入する手順をまとめました。
J2JHでは今回触れたこと以外にもユーザリソースの設定アクセス制限JupyterLabを標準で使用するなど、様々な設定が可能です。
詳細が気になる方はCustomization GuideAdministrator Guideをご確認ください。