Audio Overview(English/日本語)
English (recommended)
日本語(非推奨)
はじめに
コードを迅速にプロトタイピングしたい時やデータを可視化して共有したい時に、手軽に利用できるGoogle Colabのようなサービスをself-hostedで提供したいと考えています。
特に研究室サーバーではホストマシン上にPythonがインストールされておらず、JupyterLabを利用したい場合はDockerコンテナを起動する必要があります。
環境の分離という観点では正しい運用ですが、設定の煩わしさからGoogle Colabに勝るユーザー体験は提供できていません。Docker / Podmanに慣れていないメンバーや研究室に新規参入したメンバーでも研究活動をbootstrapできるようなプラットフォームが理想的です。
本記事では、Kubeflow Notebooksを利用してオンプレミスKubernetesクラスタにNotebook as a Serviceを構築する方法とユーザー体験を向上させるための工夫について紹介します。
- はじめに
- 1. Kubeflow Notebooksの概要
- 2. JupyterHubとの比較
- 3. Kubeflowのインストール
- 4. Kubeflow Notebooksの動作検証
- 5. marimoカスタムイメージの利用
- 6. HTTPSによるサービス公開
- 7. DexによるGitHub認証
- 8. ExternalDNSによるCloudflare DNSの利用
- まとめ
1. Kubeflow Notebooksの概要
Kubeflowは、Kubernetes上でAI/MLを簡単に、柔軟に、そして拡張可能に実行するための、機械学習ライフサイクル全体をサポートするオープンソースのプロジェクト群です。
Kubeflow NotebooksはそんなKubeflowコンポーネントの1つで、Webベースの開発環境を提供できます。
実際にはKubernetesクラスタ上でPodを起動していますが、以下の特徴があります。
- JupyterLab、RStudio、code-serverのネイティブサポート
- ユーザーがクラスタ上で直接ノートブックコンテナを作成可能
- 管理者がカスタムノートブックイメージを提供できる
- アクセス制御をKubeflowのRBACで管理できるためノートブックの共有が容易
特にブラウザ上でVS Codeが利用できるcode-serverをサポートしている点や、必要なパッケージがプリインストールされたカスタムイメージを利用できる点が魅力的だと感じました。
2. JupyterHubとの比較
JupyterHubは、シングルユーザー用Jupyter notebook(またはJupyterLab)サーバーを複数インスタンス起動・管理・プロキシする機能を提供することで、マルチユーザーをサポートしています。
JupyterHubをデプロイする方法としては、以下の2種類が用意されています。
- The Littlest JupyterHub(TLJH):単一VM上、少数ユーザー向け
- Zero to JupyterHub with Kubernetes(Z2JH):Kubernetes上、多数ユーザー向け
公式ドキュメントによると、TLJHは単一マシンでコンテナを利用しない場合、Z2JHは複数マシンでコンテナを利用する場合の利用が推奨されています。
Kubeflow NotebooksとZ2JHを比較します。
Kubeflow Notebooks | JupyterHub (Z2JH) | |
---|---|---|
アーキテクチャ | Notebook ControllerがNotebook カスタムリソースの作成をトリガーにStatefulSet / Service / VirtualService を作成 |
HubがKubespawnerを利用してシングルユーザー用のノートブックサーバーをPod として起動し、Configurable HTTP Proxyがルーティングテーブルを更新 |
認証 | Istio Ingress Gateway + OAuth2-Proxy (OIDC client) + Dex (OIDC IdP) | Ingress + Configurable HTTP Proxy + Authenticator |
IDEサポート | JupyterLab、RStudio、code-serverをネイティブサポート | JupyterLabとJupyter notebookを標準サポート(Jupyter Server ProxyでRStudioやcode-serverも利用可能) |
カスタムイメージ | ユーザーがノートブック作成時にイメージ名を指定可能 | ユーザーが選択可能なプロファイルを管理者がKubeSpawnerの設定として定義 |
リソース指定 | ユーザーがノートブック作成時にCPU、メモリ、GPU、ボリュームを指定可能 |
JupyterHub(Z2JH)はHelmで容易にインストールできますが、ユーザーが自由にリソースやカスタムイメージを指定できないことや、ノードメンテナンス時にPodが自動で再作成されないことに注意が必要です。
Kubeflow NotebooksはKubeflow Platformのコンポーネントが多いため学習コストが大きくなりますが、StatefulSetの恩恵を受けつつ、Web UIでユーザーが自由にリソース・カスタムイメージを指定できます。
以上の理由から、今回はNaaSユーザーにとってカスタマイズ性が高いKubeflow Notebooksを採用しました。
3. Kubeflowのインストール
#14で構築したオンプレミスKubernetesクラスタにKubeflow Notebooksをインストールします。
Kubeflow Notebooksはstandaloneコンポーネントとしてインストールできないので、Kubeflow Platformとして全てのコンポーネントをインストールします。
執筆時点で最新の安定版であるKubeflow Platform v1.10.0
を利用します。コンポーネント数が多いため、READMEの通りにシングルコマンドによるインストールを実行しました。
$ while ! kustomize build example | kubectl apply --server-side --force-conflicts -f -; do echo "Retrying to apply resources"; sleep 20; done # 省略
インストール完了後に2つの問題が発生しました。
1つ目の問題として、Istioを利用して公開していたArgo CDなどのサービスがRBAC: access denied
エラーを返すようになりました。最初は2つのIngress Gatewayがデプロイされていることが原因だと考えていましたが、Tetrateのドキュメントにもあるように複数のIngress Gatewayをデプロイすることは可能です。
権限エラーであることからも分かるように、実際はKubeflow Platformのマニフェストでallow-nothingのAuthorizationPolicyを設定していることが原因でした。Kubeflow Platform用のIngress Gatewayだけでなく、既存のIngress Gatewayへのトラフィックも許可するALLOW
ポリシーを追加する必要があります。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: istio-ingressgateway-private namespace: istio-system spec: action: ALLOW selector: matchLabels: istio: ingressgateway-private # 適宜変更 rules: - {}
これによって1つ目の問題は解決しました。
2つ目の問題として、Kubeflow関連のいくつかのPodでコンテナの作成に失敗していました。
$ kubectl get pods -n kubeflow | awk '$3!="Running"' NAME READY STATUS RESTARTS AGE katib-db-manager-6987b965b7-tfnwp 0/1 Error 193 (6m24s ago) 15h katib-mysql-5dfcbbc87f-xjnkb 0/1 Pending 0 15h metadata-grpc-deployment-6c44975f56-f2ffs 1/2 CrashLoopBackOff 190 (2m32s ago) 15h metadata-writer-6fbc8d8c4f-x4m8b 1/2 CrashLoopBackOff 143 (39s ago) 15h minio-6748f5ff9d-5xslh 0/2 Pending 0 15h ml-pipeline-857d4dd86-qggb6 1/2 CrashLoopBackOff 238 (4m5s ago) 15h mysql-6c6bb95f89-89r29 0/2 Pending 0 15h
CLIでの原因調査
$ kubectl logs ml-pipeline-857d4dd86-qggb6 -n kubeflow I0504 06:39:26.476455 7 client_manager.go:170] Initializing client manager I0504 06:39:26.476614 7 client_manager.go:171] Initializing DB client... I0504 06:39:26.476660 7 config.go:57] Config DBConfig.MySQLConfig.ExtraParams not specified, skipping [mysql] 2025/05/04 06:39:26 connection.go:49: unexpected EOF # 省略 $ kubectl describe pod mysql-6c6bb95f89-89r29 -n kubeflow # 省略 Warning FailedScheduling 2m30s (x202 over 16h) default-scheduler 0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling. $ kubectl get pvc -n kubeflow NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE katib-mysql Pending <unset> 16h minio-pvc Pending <unset> 16h mysql-pv-claim Pending <unset> 16h $ kubectl describe pvc mysql-pv-claim -n kubeflow # 省略 Normal FailedBinding 100s (x4002 over 16h) persistentvolume-controller no persistent volumes available for this claim and no storage class is set $ kubectl get pv No resources found $ kubectl get sc No resources found
PersistentVolumeClaim(PVC)がPersistentVolume(PV)をバインドできず、MySQLやMinIOなどのPodがPending状態のままでした。そのため、これらのストレージに依存するサービスで接続エラーが発生していました。
再確認したところ、READMEのPrerequisitesのうち以下の項目を見落としていました。
- Either our local Kind (installed below) or your own Kubernetes cluster with a default StorageClass.
ボリュームタイプとしては、すぐに用意できるhostPathやlocalを初期候補として検討しました。ただし、手動でPVを個別作成する手間を省くため以下のProvisionerを導入しました。
Local Path Provisionerによって、PVCに対応したPVがノード上に自動的に作成されます。
今回のPVCのマニフェストにはstorageClassName
が指定されていないため、ターゲットとなるStorageClassをデフォルトに指定することで全てのPodが無事にRunning状態となりました。
$ kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-path rancher.io/local-path Delete WaitForFirstConsumer false 7h29m $ kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' storageclass.storage.k8s.io/local-path patched $ kubectl get pvc -n kubeflow NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE katib-mysql Bound pvc-fee92883-d2ef-451c-aa71-c432e935703f 10Gi RWO local-path <unset> 17h minio-pvc Bound pvc-39c3a5ee-6e29-44ca-b689-820f437c782d 20Gi RWO local-path <unset> 17h mysql-pv-claim Bound pvc-c8423a2c-e2a6-42ce-a05f-a55bf42f82f9 20Gi RWO local-path <unset> 17h $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pvc-39c3a5ee-6e29-44ca-b689-820f437c782d 20Gi RWO Delete Bound kubeflow/minio-pvc local-path <unset> 2m1s pvc-c8423a2c-e2a6-42ce-a05f-a55bf42f82f9 20Gi RWO Delete Bound kubeflow/mysql-pv-claim local-path <unset> 2m2s pvc-fee92883-d2ef-451c-aa71-c432e935703f 10Gi RWO Delete Bound kubeflow/katib-mysql local-path <unset> 119s $ kubectl get pods -n kubeflow | awk '$3!="Running"' NAME READY STATUS RESTARTS AGE
これによって2つ目の問題も解決し、無事にKubeflow Platformのインストールが完了しました。
4. Kubeflow Notebooksの動作検証
Web UI上でKubeflow Notebooksの動作検証を行います。
ただしREADMEで言及されている通り、localhost以外ではHTTPでKubeflowにアクセスできません。別のIngress経由でistio-ingressgateway
サービスをHTTP公開する方法もありますが、6章でサービス公開のセットアップをする予定なのでしばらくはkubectl portforward
コマンドを検証用に利用します。
普段コントロールプレーンノード(以下の例ではpolaris
)にSSH接続して作業している場合は、SSHのローカルポートフォワーディングと組み合わせて利用できます。
azuma@macbook $ ssh -L 8080:localhost:8080 polaris root@polaris $ kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80 Forwarding from 127.0.0.1:8080 -> 8080 Forwarding from [::1]:8080 -> 8080
MacBookのWebブラウザからlocalhost:8080
でKubeflowのWeb UIにアクセスできます。
最新版とスクリーンショットが少し異なっていますが、概ね上記の公式ドキュメント通りにノートブックを作成できます。
最もシンプルな設定として、GPUもWorkspace Volumeも無いJupyterLabノートブックを作成します。
LauncherからPython 3 (ipykernel)を選択して、JupyterLabが問題なく利用できることを確認しました。
次に、PVCをボリュームとして利用できることを検証します。
ノートブックを作成すると、PVCに対応したPVがLocal Path Provisionerによって自動的に作成されていることが分かります。
$ kubectl get pvc -n kubeflow-user-example-com NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE test-datavol-1 Bound pvc-c2b5305e-217d-4134-b78a-3b246a07060b 5Gi RWO local-path <unset> 3m46s test-workspace Bound pvc-bc09bf68-dc42-4d94-8d50-82fba5d1adeb 5Gi RWO local-path <unset> 3m46s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pvc-bc09bf68-dc42-4d94-8d50-82fba5d1adeb 5Gi RWO Delete Bound kubeflow-user-example-com/test-workspace local-path <unset> 3m53s pvc-c2b5305e-217d-4134-b78a-3b246a07060b 5Gi RWO Delete Bound kubeflow-user-example-com/test-datavol-1 local-path <unset> 3m53s # 省略
Web UI上で設定した通りのパスにWorkspace VolumeとData Volumesがそれぞれマウントされていることをノートブックからも確認できました。
最後に、GPUが利用できることを検証します。
カスタムイメージとしてjupyter-pytorch-cuda-full:v1.10.0を選択してノートブックを作成すると、以下のようなエラーが出てコンテナが作成できませんでした。
RunContainerError: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error running prestart hook #0: exit status 1, stdout: , stderr: Auto-detected mode as 'legacy' nvidia-container-cli: requirement error: unsatisfied condition: cuda>=12.4, please update your driver to a newer version, or use an earlier cuda container: unknown
jupyter-pytorch-cudaはDockerfile内でCUDA 12.4に対応するPyTorchをインストールしており、cuda>=12.4
を要求しています。一方、全てのワーカーノードでDriverバージョンは535.216.03
であり、CUDA 12.4には>=550.54.14
が必要です(参考)。
解決策として、新しいバージョンのDriverにアップデートするか、古いCUDAコンテナを利用するかの2通りの方法が考えられます。前者はノードメンテナンスが必要となり、他のメンバーの研究を中断してしまうため、今回は後者で対応することにしました。
Kubeflowのv1.9
のDockerfileでは要求がcuda>=12.1
になっていたため、コンテナイメージのタグを変更することにしました。マニフェストにspawner_ui_config.yamlというUIの設定ファイルがあるので以下のように修正して差分を適用します。
image: # the default container image - value: ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.10.0 + value: ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-full:v1.9.0 # the list of available container images in the dropdown options: - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.10.0 - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-full:v1.10.0 - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-cuda-full:v1.10.0 - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-gaudi-full:v1.10.0 - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-tensorflow-full:v1.10.0 - - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-tensorflow-cuda-full:v1.10.0 + - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.9.0 + - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-full:v1.9.0 + - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-cuda-full:v1.9.0 + - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-tensorflow-full:v1.9.0 + - ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-tensorflow-cuda-full:v1.9.0
ついでにlogos-configmap.yamlで、VSCodeとRStudioのアイコン・ロゴも追加するとUXが改善します。
jupyter-pytorch-cuda-full:v1.9.0コンテナイメージを使ってGPUを2枚に設定したJupyterLabノートブックを作成したところ、無事にGPUが利用可能であることを確認できました。
以上でKubeflow Notebooksの動作検証は完了です。
5. marimoカスタムイメージの利用
#13でも紹介しているように、最近はJupyter notebookやJupyterLabの課題を解決するPythonノートブックとして再現性重視でGitフレンドリーなmarimoが注目されています。
Kubeflow Notebooksでも選択肢の1つとしてmarimoを利用できるようにカスタムイメージを作成します。
上記のKubeflow公式ドキュメントやREADMEによると、Common Base Imageを拡張したBaseイメージ(JupyterLab、code-server、RStudio)と、主にJupyterLabに対してPyTorchやTensorflowなどのパッケージをインストールして拡張したKubeflowイメージが提供されています。
一方、カスタムイメージを作成する際にはBaseイメージの拡張が推奨されており、以下の3つの要件を満たす必要があります。
- HTTPインターフェースをポート
8888
で公開 jovyan
と呼ばれるユーザーで実行/home/jovyan
にマウントされた空のPVCで正常に起動
Common Base Imageを拡張している他のBaseイメージを参考にしてmarimoのカスタムイメージを作成しました。変更は以下のPRから確認できます。
Kubeflowにコントリビュートするチャンスですが、イメージ追加以外にも修正なので一旦保留としました。具体的にはフロントエンドとバックエンドで、JupyterLab・code-server・RStudioの3種類がそれぞれjupyter
・group-one
・group-two
としてハードコーディングされており、フォームの送信やIstioによるURI書き換えの際に必要となります。参考程度に影響のあるファイルをいくつか紹介します。
- components/crud-web-apps/jupyter/frontend/src/app/pages/form/form-new/form-image/form-image.component.html
- components/crud-web-apps/jupyter/frontend/src/app/types/notebook.ts
- components/crud-web-apps/jupyter/backend/apps/common/form.py
marimoのアイコンをWeb UI上に表示して利用できるようにするには手間がかかりそうですが、イメージを利用するだけであれば2通りの方法があります。1つはフォーム上でCustomイメージとしてイメージ名を入力する方法、もう1つはJupyterLabのイメージグループに追加する方法です。本質的にそれほど大きな差はないので、ユーザー体験を考慮して後者の方法を採用します。
4章に続けて、マニフェストのspawner_ui_config.yamlという設定ファイルを変更して適用します。
################################################################
# Jupyter-like Container Images
#
# NOTES:
# - the `image` section is used for "Jupyter-like" apps whose
# HTTP path is configured by the "NB_PREFIX" environment variable
################################################################
image:
# the default container image
value: ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-pytorch-full:v1.9.0
# the list of available container images in the dropdown
options:
- ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.9.0
# 省略
+ - ghcr.io/kitsuyaazuma/kubeflow/notebook-servers/marimo:latest
Package kubeflow/notebook-servers/marimo · GitHub
marimoのカスタムイメージは上記のGitHubコンテナレジストリで公開しています。
また、提供されているjupyter-pytorch
やjupyter-pytorch-cuda-full
などのイメージが全てCondaを採用しているのに対し、marimo
カスタムイメージではuvを採用しているためユーザー体験の向上が期待できます。
以上で、作成したmarimoのカスタムイメージをKubeflow Notebooksから利用できることを確認できました。
6. HTTPSによるサービス公開
4章でも言及しましたが、KubeflowではWebアプリでSecure Cookieを利用しているため、HTTPSのセットアップが推奨されています。KubeflowのサービスをHTTPSで公開するため、マニフェストリポジトリのREADMEでも紹介されているようにIngressでIstio Ingress Gatewayを公開しながらTLS終端を行います。
Ingressの役割については以下のKubernetes公式ドキュメントが参考になります。
まずはNGINX Ingress Controllerをインストールします。
次にMetalLBをインストールして、NGINX Ingress ControllerをLoadBalancerサービスとして公開しましたが、NodePortを利用する場合は適宜読み替えてください。
公式ドキュメントに従ってMetalLBをインストールした後、IPAddressPool
とL2Advertisement
という2つのカスタムリソースを作成します。
apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: example-ipaddresspool namespace: metallb-system spec: addresses: - 192.168.1.XXX-192.168.1.YYY --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: example-l2advertisement namespace: metallb-system spec: ipAddressPools: - example-ipaddresspool
次に、手動で自己証明書を作成してSecretリソースを作成します。今回はローカルで信頼される証明書を発行できるmkcertを利用しました。
$ mkcert -install The local CA is now installed in the system trust store! ⚡️ $ mkcert nlab.com Created a new certificate valid for the following names 📜 - "nlab.com" The certificate is at "./nlab.com.pem" and the key at "./nlab.com-key.pem" ✅ It will expire on 16 August 2027 🗓 $ kubectl create -n ingress-nginx secret tls tls-secret \ --key=nlab.com-key.pem \ --cert=nlab.com.pem secret/tls-secret created
Ingressリソースを作成してspec.tls
フィールドでTLS終端の設定を行います。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress spec: ingressClassName: nginx rules: - host: nlab.com http: paths: - path: / pathType: Prefix backend: service: name: example-service port: number: 80 tls: - hosts: - nlab.com secretName: tls-secret --- # https://kubernetes.io/docs/concepts/services-networking/service/#externalname apiVersion: v1 kind: Service metadata: name: example-service spec: type: ExternalName externalName: istio-ingressgateway.istio-system.svc.cluster.local ports: - port: 80
Ingressリソースを確認すると、MetalLBによって外部IPアドレスが割り当てられていることが分かります。
$ kubectl get ingress -n ingress-nginx NAME CLASS HOSTS ADDRESS PORTS AGE example-ingress nginx nlab.com 192.168.1.XXX 80, 443 6m9s
外部IPアドレスは割り当てられましたが名前解決はできていないので、MacBookの/etc/hosts
に192.168.1.XXX nlab.com
のように追記してください。将来的には(Cloudflare DNSなどを利用する)ExternalDNSを導入することで、ユーザーの負担を軽減したいです。
アドレスバーにNot Secureという警告が出る場合
MacBookの場合、ルート証明書をダウンロードしてからKeychain Accessで信頼されたルート証明書として設定します。
root@polaris
$ mkcert -CAROOT
/root/.local/share/mkcert
azuma@macbook
$ scp polaris:/root/.local/share/mkcert/rootCA.pem .
$ open rootCA.pem
以上で、KubeflowのサービスをHTTPSで公開することができました。
7. DexによるGitHub認証
これまではKubeflow Central Dashboardにアクセスすると、Dexのログイン画面が起動してデフォルトのユーザー名user@example.com
とパスワード12341234
でログインしていました。しかし実際に運用する場合は、たとえ研究室内であってもデフォルトユーザーを全員で使い回すことはあり得ません。
Dexはユーザーを他のIdPで認証するためのConnectorを実装しており、LDAPやOIDC、OAuth 2.0といったプロトコルベースのものから、GitHubやGoogle、Microsoftのようなプラットフォーム固有のものまでサポートしています。詳細は公式ドキュメントを参照してください。
今回は、既に研究室のGitHub Organizationが存在していて大多数のメンバーが参加していることから、GitHub認証を利用することにしました。
READMEと上記の公式ドキュメントに従って、マニフェストのconfig-map.yamlを以下のように変更しました。
apiVersion: v1 kind: ConfigMap metadata: name: dex data: config.yaml: | - issuer: http://dex.auth.svc.cluster.local:5556/dex + issuer: https://nlab.com/dex # 省略 staticClients: # https://github.com/dexidp/dex/pull/1664 - idEnv: OIDC_CLIENT_ID redirectURIs: ["/oauth2/callback"] name: 'Dex Login Application' secretEnv: OIDC_CLIENT_SECRET + connectors: + - type: github + id: github + name: GitHub + config: + clientID: $GITHUB_CLIENT_ID + clientSecret: $GITHUB_CLIENT_SECRET + redirectURI: https://nlab.com/dex/callback + loadAllGroups: false + teamNameField: slug + useLoginAsID: false
$GITHUB_CLIENT_ID
と$GITHUB_CLIENT_SECRET
は適宜置き換えてください。GitHub OrganizationのDeveloper Settingsから新しいOAuth Appを作成する必要があります。Authorization callback URLは上記のConfigMapのredirectURI
と一致させる必要があります。
Kubeflowの認証をカスタムする技術記事はほとんど無く、DexでConnectorを追加した場合に修正すべきマニフェストはREADMEにも記載されていません。今回はKeyCloak Integration Guideを参考にしながら、以下のようにOAuth2 ProxyとIstio Request Authenticationの設定を変更しました。
(差分)common/oauth2-proxy/base/oauth2_proxy.cfg
provider = "oidc" -oidc_issuer_url = "http://dex.auth.svc.cluster.local:5556/dex" +oidc_issuer_url = "https://nlab.com/dex" scope = "profile email groups openid"
(差分)common/oauth2-proxy/components/istio-external-auth/requestauthentication.dex-jwt.yaml
jwtRules: - - issuer: http://dex.auth.svc.cluster.local:5556/dex + - issuer: https://nlab.com/dex + jwksUri: http://dex.auth.svc.cluster.local:5556/dex/keys
IstioのRequest AuthenticationはデフォルトでIssuerの/.well-known/openid-configuration
から公開鍵を取得しますが、ブラウザと違ってクラスター内のPodからはnlab.com
が名前解決ができないので、明示的にjwksUri
フィールドをdex.auth.svc.cluster.local
と指定しなければいけない点がポイントでした。
v1.10は図中の「kubeflow 1.9 with oauth2 proxy」に該当
仕組みとしては、ユーザーがブラウザからKubeflowにアクセスするとIstioのEnvoyに到達し、ext_authz
フィルターによってOAuth2 Proxyにリクエストが転送されます。OAuth2 ProxyはセッションCookie(oauth2_proxy_kubeflow
)を検証して、未認証であればDexを介して上流IdP(GitHubなど)とOIDC / OAuth2フローを実行してトークンレスポンス(IDトークンやリフレッシュトークンを含む)を取得します。OAuth2 ProxyはIDトークンをもとにCookieを発行し、以降のリクエストではそのCookieの検証を行います。最終的にはOAuth2 Proxyが付与する認証済みヘッダーをEnvoyが受け取り正当なユーザーとしてバックエンドのサービスへリクエストを通過させます。
現時点でユーザーはログインしてもProfileがないためノートブックを作成できません。ProfileカスタムリソースはNamespaceをラップしたもので、1人のユーザーがProfileを所有しますが、コントリビューターに権限を付与することもできます。
公式ドキュメントに従って環境変数の値を変更し、デフォルトで無効化されているProfileの自動作成を有効化します。再度新しいユーザーでログインすると、Namespaceの作成画面が表示されるはずです。
$ kubectl get namespace | grep kitsuyaazuma
kitsuyaazuma Active 64s
GitHub認証を利用することで、DexのBuilt-In ConnectorでのStaticなユーザー管理を必要とせず、マルチユーザーにNaaSを提供できるようになりました。研究室用途であればこれでも十分ですが、
8. ExternalDNSによるCloudflare DNSの利用
6章では、ドメイン名(自己証明書を作成したnlab.com
)を利用してKubeflowダッシュボードにHTTPSでアクセスできるようになりました。しかしユーザー視点では、名前解決のために/etc/hosts
でホスト名とIPアドレスを関連付けなければならず、ブラウザの警告を消すために手動でルート証明書を信頼しなければなりません。
これらの問題を一気に解決するために、ExternalDNSを利用してCloudflare DNSのレコードを動的に更新しつつ、証明書管理も自動化します。
事前にCloudflareでドメインを登録して、サイドバーの「アカウントの管理」からAPIトークンを発行する必要があります。下記の公式チュートリアルに取り組んで、NginxにHTTPでアクセスできることを確認しましょう。注意点として、cert-managerが既にデフォルトでインストールした場合はAPIトークンのSecretをcert-manager
Namespaceに作成しておきましょう(参考)。
次に、cert-managerで証明書管理を自動化します。今回は、ClusterIssuerでACME認証局(例:Let's Encrypt)とのチャレンジ方式(例:DNS-01チャレンジ)を設定し、Certificateで証明書を発行するDNS名や保存するSecret名、参照するIssuerなどを設定します。
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: email: YOUR_EMAIL server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: issuer-account-key solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-api-key key: apiKey --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: example-com namespace: ingress-nginx spec: dnsNames: - YOUR_DOMAIN_NAME issuerRef: group: cert-manager.io kind: ClusterIssuer name: letsencrypt secretName: example-com-tls
これに合わせて、Ingressのrules
&tls
フィールド、7章で変更したKubeflow認証のissuer
フィールド、GitHub OAuth Appのcallback URLを変更する必要があります。YOUR_DOMAIN_NAME
は研究室ホームページに利用したいという要望があったので、このタイミングでサブドメインのkubeflow.YOUR_DOMAIN_NAME
を利用する方針に変更しました。
https://kubeflow.{ドメイン名}
でKubeflowにアクセス
以上で、動的なDNSレコード更新と証明書管理の自動化を達成することができました。
まとめ
Kubeflow Notebooksを利用して研究室サーバーにNotebook as a Serviceを構築する方法を紹介しました。
執筆時点でこのNaaSをα版としてリリースしており、一部の研究室メンバーに利用して頂くことができました。今年度のうちにβ版リリース、そしてGAを目指します。また、機能の追加だけでなくドキュメントの整備も含めた総合的なユーザー体験の向上に努めていきます。
今後の課題としては以下の候補が挙げられます。
- 直感的にBaseイメージを拡張してカスタムイメージをpushできるワークフローの構築
- HarborやDistributionによるプライベートレジストリのデプロイ
- NFSボリュームの利用と動的プロビジョニング
Kubeflow Platformのコンポーネントは非常に多く、JupyterHub(Z2JH)と比較して学習コストがかかります。しかしKubeflowはKubernetesネイティブなアーキテクチャであるため、今まで触れてこなかった技術(私の場合はIstioやOIDC、TLS)に向き合う非常に良いチャンスになります。本記事が、社内のデータプラットフォームや逸般の研究室サーバーで参考になれば幸いです。
【告知】CloudNative Days 2025にて『研究室サーバーとKubeflowで実践するNotebook as a Service』というテーマでLT発表します。