KubeflowによるNaaS構築入門(#16)

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の概要

Kubeflowは、Kubernetes上でAI/MLを簡単に、柔軟に、そして拡張可能に実行するための、機械学習ライフサイクル全体をサポートするオープンソースのプロジェクト群です。

Kubeflow NotebooksはそんなKubeflowコンポーネントの1つで、Webベースの開発環境を提供できます。

www.kubeflow.org

Kubeflow NotebooksのWeb UIのスクリーンショット

実際にはKubernetesクラスタ上でPodを起動していますが、以下の特徴があります。

  • JupyterLabRStudiocode-serverのネイティブサポート
  • ユーザーがクラスタ上で直接ノートブックコンテナを作成可能
  • 管理者がカスタムノートブックイメージを提供できる
  • アクセス制御をKubeflowのRBACで管理できるためノートブックの共有が容易

特にブラウザ上でVS Codeが利用できるcode-serverをサポートしている点や、必要なパッケージがプリインストールされたカスタムイメージを利用できる点が魅力的だと感じました。

2. JupyterHubとの比較

JupyterHubは、シングルユーザー用Jupyter notebook(またはJupyterLab)サーバーを複数インスタンス起動・管理・プロキシする機能を提供することで、マルチユーザーをサポートしています。

JupyterHubをデプロイする方法としては、以下の2種類が用意されています。

公式ドキュメントによると、TLJHは単一マシンでコンテナを利用しない場合、Z2JHは複数マシンでコンテナを利用する場合の利用が推奨されています。

Kubeflow NotebooksとZ2JHを比較します。

Kubeflow Notebooks JupyterHub (Z2JH)
アーキテクチャ Notebook ControllerNotebookカスタムリソースの作成をトリガーに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をインストールします。

www.kubeflow.org

Kubeflow Notebooksはstandaloneコンポーネントとしてインストールできないので、Kubeflow Platformとして全てのコンポーネントをインストールします。

github.com

執筆時点で最新の安定版である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-nothingAuthorizationPolicyを設定していることが原因でした。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のうち以下の項目を見落としていました。

ボリュームタイプとしては、すぐに用意できるhostPathlocalを初期候補として検討しました。ただし、手動でPVを個別作成する手間を省くため以下のProvisionerを導入しました。

github.com

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

MacBookWebブラウザからlocalhost:8080でKubeflowのWeb UIにアクセスできます。

www.kubeflow.org

最新版とスクリーンショットが少し異なっていますが、概ね上記の公式ドキュメント通りにノートブックを作成できます。

最もシンプルな設定として、GPUもWorkspace Volumeも無いJupyterLabノートブックを作成します。

ノートブックの一覧画面

ノートブック接続後の画面

LauncherからPython 3 (ipykernel)を選択して、JupyterLabが問題なく利用できることを確認しました。

次に、PVCをボリュームとして利用できることを検証します。

Web UI上でWorkspace VolumeとData Volumesを設定

ノートブックを作成すると、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が利用できることを検証します。

WebUI上でGPUsを設定

カスタムイメージとして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.9Dockerfileでは要求が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で、VSCodeRStudioのアイコン・ロゴも追加するとUXが改善します。

コンテナイメージを変更して、ロゴまで追加した結果

jupyter-pytorch-cuda-full:v1.9.0コンテナイメージを使ってGPUを2枚に設定したJupyterLabノートブックを作成したところ、無事にGPUが利用可能であることを確認できました。

PyTorchでGPUが利用できることを確認

以上でKubeflow Notebooksの動作検証は完了です。

5. marimoカスタムイメージの利用

#13でも紹介しているように、最近はJupyter notebookやJupyterLabの課題を解決するPythonノートブックとして再現性重視でGitフレンドリーなmarimoが注目されています。

marimo.io

Jupyter notebook、JupyterLab、marimoのGitHubスター数の比較(リンク

Kubeflow Notebooksでも選択肢の1つとしてmarimoを利用できるようにカスタムイメージを作成します。

www.kubeflow.org

上記の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から確認できます。

github.com

Kubeflowにコントリビュートするチャンスですが、イメージ追加以外にも修正なので一旦保留としました。具体的にはフロントエンドとバックエンドで、JupyterLab・code-server・RStudioの3種類がそれぞれjupytergroup-onegroup-twoとしてハードコーディングされており、フォームの送信やIstioによるURI書き換えの際に必要となります。参考程度に影響のあるファイルをいくつか紹介します。

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コンテナレジストリで公開しています。

JupyterLabのイメージグループの中からmarimoイメージを選択

ノートブックに接続してmarimoを利用

また、提供されているjupyter-pytorchjupyter-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公式ドキュメントが参考になります。

kubernetes.io

まずはNGINX Ingress Controllerをインストールします。

github.com

次にMetalLBをインストールして、NGINX Ingress ControllerをLoadBalancerサービスとして公開しましたが、NodePortを利用する場合は適宜読み替えてください。

(任意)MetalLBのインストール MetalLBのインストールすることで、type: LoadBalancerなサービスに外部IPアドレスを割り当てることができます。

kubernetes.github.io

metallb.io

公式ドキュメントに従ってMetalLBをインストールした後、IPAddressPoolL2Advertisementという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/hosts192.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

追加したルート証明書をダブルクリックする

Always Trustを選択する

https://{ドメイン名}でKubeflowにアクセス

以上で、KubeflowのサービスをHTTPSで公開することができました。

7. DexによるGitHub認証

これまではKubeflow Central Dashboardにアクセスすると、Dexのログイン画面が起動してデフォルトのユーザー名user@example.comとパスワード12341234でログインしていました。しかし実際に運用する場合は、たとえ研究室内であってもデフォルトユーザーを全員で使い回すことはあり得ません。

Dexはユーザーを他のIdPで認証するためのConnectorを実装しており、LDAPやOIDC、OAuth 2.0といったプロトコルベースのものから、GitHubGoogleMicrosoftのようなプラットフォーム固有のものまでサポートしています。詳細は公式ドキュメントを参照してください。

今回は、既に研究室のGitHub Organizationが存在していて大多数のメンバーが参加していることから、GitHub認証を利用することにしました。

dexidp.io

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と一致させる必要があります。

OAuth Appの設定例(Homepage URLは任意のURLで可)

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と指定しなければいけない点がポイントでした。

Kubeflowの認証フロー(引用:kubeflow/manifests
v1.10は図中の「kubeflow 1.9 with oauth2 proxy」に該当

仕組みとしては、ユーザーがブラウザからKubeflowにアクセスするとIstioのEnvoyに到達し、ext_authzフィルターによってOAuth2 Proxyにリクエストが転送されます。OAuth2 ProxyはセッションCookieoauth2_proxy_kubeflow)を検証して、未認証であればDexを介して上流IdP(GitHubなど)とOIDC / OAuth2フローを実行してトークンレスポンス(IDトークンやリフレッシュトークンを含む)を取得します。OAuth2 ProxyはIDトークンをもとにCookieを発行し、以降のリクエストではそのCookieの検証を行います。最終的にはOAuth2 Proxyが付与する認証済みヘッダーをEnvoyが受け取り正当なユーザーとしてバックエンドのサービスへリクエストを通過させます。

GitHub認証でパスキーを利用して簡単にKubeflowにログイン

現時点でユーザーはログインしてもProfileがないためノートブックを作成できません。ProfileカスタムリソースはNamespaceをラップしたもので、1人のユーザーがProfileを所有しますが、コントリビューターに権限を付与することもできます。

www.kubeflow.org

公式ドキュメントに従って環境変数の値を変更し、デフォルトで無効化されているProfileの自動作成を有効化します。再度新しいユーザーでログインすると、Namespaceの作成画面が表示されるはずです。

デフォルトでユーザー名の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のレコードを動的に更新しつつ、証明書管理も自動化します。

github.com

事前にCloudflareでドメインを登録して、サイドバーの「アカウントの管理」からAPIトークンを発行する必要があります。下記の公式チュートリアルに取り組んで、NginxにHTTPでアクセスできることを確認しましょう。注意点として、cert-managerが既にデフォルトでインストールした場合はAPIトークンのSecretをcert-manager Namespaceに作成しておきましょう(参考)。

github.com

チュートリアルのサービスにHTTPでアクセスできるかを確認

次に、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

これに合わせて、Ingressrulestlsフィールド、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できるワークフローの構築
  • HarborDistributionによるプライベートレジストリのデプロイ
  • NFSボリュームの利用と動的プロビジョニング

Kubeflow Platformのコンポーネントは非常に多く、JupyterHub(Z2JH)と比較して学習コストがかかります。しかしKubeflowはKubernetesネイティブなアーキテクチャであるため、今まで触れてこなかった技術(私の場合はIstioやOIDC、TLS)に向き合う非常に良いチャンスになります。本記事が、社内のデータプラットフォームや逸般の研究室サーバーで参考になれば幸いです。

【告知】CloudNative Days 2025にて『研究室サーバーとKubeflowで実践するNotebook as a Service』というテーマでLT発表します。