JupyterHub on Kubernetes: Vaultでノートブックのシークレットを安全に扱う
この投稿では、Kubernetesホームラボ上にマルチユーザー対応のJupyterHubを構築し、日常的に使える形に仕上げます。Helm(Justレシピでラップ)でインストールし、ユーザープロファイルとカスタムイメージを有効化し、PostgreSQLなどのクラスタ内サービスへ接続し、Vaultと小さなPythonヘルパーでAPIキーをノートブックから安全に扱います。最終的に、SSO、適切なデフォルト設定、快適な開発体験を備えたセルフホストのノートブック基盤を構築します。
このシリーズの以前の投稿を進めていれば、k3sクラスタ、OIDC用のKeycloak、Vault、(必要であれば)Longhornが動作しているはずです。ここではそれらを前提に進めます。
リポジトリ: https://github.com/buun-ch/buun-stack
JupyterHubの概要
JupyterHubはJupyterのマルチユーザーゲートウェイです。Kubernetes上ではユーザーごとにPodが起動されます。ファイルは永続ボリューム(PVC)に保存され、各ユーザーサーバーへのルーティングはプロキシが担います。認証はプラグインとして用意され、この構成ではKeycloak(OIDC)で認証します。このモデルにより、管理は一元化しつつ、各ユーザーに独立した再現性のある環境を提供できます。
小規模チームやホームラボにとっての利点は明確です。サインインとアクセス管理の入口は1つに集約され、ユーザーは自分の作業に合った環境を選ぶことができます。データはローカルに保持でき、性能やコストが予測しやすくなります。
Helmでのインストール
このリポジトリにはJupyterHubのインストールを自動化するJustレシピが含まれています。
リポジトリを取得して作業ディレクトリへ:
git clone https://github.com/buun-ch/buun-stack
cd buun-stack
.env.local
にKeycloakとVaultの設定があることを確認し(README.md参照)、次を実行します:
just jupyterhub::install
インストール時に確認される項目:
- OAuthコールバックに使うJupyterHubのホスト(FQDN)
- NFSボリュームを使うか(Longhornが必要)。使う場合はNFSのIPとパス
- ノートブックシークレットを扱うためのVault連携を有効にするか
Cloudflare Tunnelを使って公開する場合はパブリックホスト名エントリを追加します。
例:
- サブドメイン:
jupyter
- ドメイン:
example.com
- サービス:
https://localhost:443
- Advanced: TLS検証を無効化
URLを開きKeycloakでサインインし、サーバーを起動して動作を確認します。
カスタマイズ
JupyterHubプロファイルにより、ユーザーはサーバー起動時にイメージとリソース量を選べます。プロファイルはHelmのvaluesで定義され、KubeSpawnerに適用されます。デフォルトではImage pullerがすぐに動作完了するように、公式の“Jupyter Notebook Data Science Stack”だけが有効になっています。これに加えて、本記事ではVault連携を含むカスタムイメージ“Buun‑stack”を利用できます。
.env.local
でBuun‑stackプロファイル(必要であればCUDA版も)を有効化します:
# プロファイルの有効化(exportするか、.env.localに記載)
JUPYTER_PROFILE_BUUN_STACK_ENABLED=true
# GPU版(任意)
JUPYTER_PROFILE_BUUN_STACK_CUDA_ENABLED=true
# 既定のdatascienceイメージを無効化(任意)
JUPYTER_PROFILE_DATASCIENCE_ENABLED=false
# イメージレジストリとタグは必要に応じて.env.localに設定
IMAGE_REGISTRY=localhost:30500
JUPYTER_PYTHON_KERNEL_TAG=python-3.12-28
イメージをビルド・プッシュします:
just jupyterhub::build-kernel-images
just jupyterhub::push-kernel-images
Buun‑stackのDockerfileにはVault連携に必要なPythonコンポーネントと一般的なデータ/MLパッケージが含まれており、すぐに使い始めることができます。
Optional: NFS‑backed storage with Longhorn(任意: Longhorn + NFS)
大きなデータセットや共有データを扱う場合、Longhornを使ってNFSボリュームPVを作成しマウントできます。データをローカルに保ち、外部転送を減らし、バックアップも容易になります。以下を設定することで、インストーラがPV/PVCを作成します。
export JUPYTERHUB_NFS_PV_ENABLED=true
export JUPYTER_NFS_IP=192.168.10.1
export JUPYTER_NFS_PATH=/volume1/drive1/jupyter
just jupyterhub::install
サービス連携
JupyterHubはクラスタ内部で動くため、ノートブックからKubernetes DNS経由でサービスに直接アクセスできます。たとえばPostgreSQLなら postgresql://user:password@postgres-cluster-rw.postgres:5432/mydb
というURLで接続できます。Notebookサーバーには POSTGRES_HOST
とPOSTGRES_PORT
が環境変数として注入されるので、次のようにURLを組み立てられます。
import os
pg_host = os.getenv('POSTGRES_HOST')
pg_port = os.getenv('POSTGRES_PORT')
pg_url = f'postgresql://user:password@{pg_host}:{pg_port}/mydb'
DuckDBを使えば、Postgresをアタッチして数行のSQLでデータの移動ができます。アドホックなインポートや軽い集計に便利です。
>>> import duckdb
>>> def setup_duckdb_postgres():
... con = duckdb.connect()
... con.execute('INSTALL postgres')
... con.execute('LOAD postgres')
... con.execute(f"ATTACH '{pg_url}' AS pg (TYPE POSTGRES)")
... return con
>>> con = setup_duckdb_postgres()
>>> con.execute(f"""
... CREATE OR REPLACE TABLE pg.athlete_events AS
... SELECT * FROM read_csv_auto('{data_dir}/athlete_events.csv')
... """)
>>> con.execute("""
... SELECT Sport, COUNT(*) as count
... FROM pg.athlete_events
... GROUP BY Sport
... ORDER BY count DESC
... LIMIT 5
... """).df()
Kubernetes内のサービスと直接つながることで、低レイテンシでシンプルな構成を保てます。オブジェクトストレージや社内APIなど、他のサービスにも同様に拡張できます。
Secrets with Vault(Vaultでシークレット管理)
クラウドサービスのノートブック(例: Google Colab)は、コードからシークレットを取得する簡単な方法を提供しています。Google Colabでは次のように扱えます。
# Google Colabの例
from google.colab import userdata
openai_api_key = userdata.get('OPENAI_API_KEY')
プレーンなJupyterでは、実行時にgetpassで貼り付ける運用が一般的ですが、手作業がめんどうで、間違いも起こりやすいです。
# プレーンな Jupyter(手動貼り付け)
from getpass import getpass
openai_api_key = getpass('OpenAI API key: ')
本スタックではVaultと小さなPythonクラス SecretStore
(Buun‑stackイメージに同梱)を使い、Google Colabのような手軽さを自前で実現します。サーバー起動時にJupyterHubがユーザー専用のVaultポリシーとトークンを作成し、NOTEBOOK_VAULT_TOKEN
とVAULT_ADDR
を環境変数として注入します。SecretStore
はそれらを用いてVaultにアクセスし、長時間の実行でも適切にトークンを更新します。
使用例:
from buunstack import SecretStore
secrets = SecretStore()
secrets.put('api-keys', openai='sk-...')
openai_api_key = secrets.get('api-keys', field='openai')
セルにトークンを貼り付ける必要はなくなります。各ユーザーには分離された名前空間が割り当てられ、アクセスはVaultに監査ログとして記録されます。利用するには、インストール時にVault連携を有効化し、Buun‑stack系のカーネルイメージを選択してください。
事前に有効化する場合は .env.local
に次を追加:
JUPYTERHUB_VAULT_INTEGRATION_ENABLED=true
インストーラを実行:
just jupyterhub::install
後からセットアップする場合:
just jupyterhub::setup-vault-jwt-auth
実装詳細(SecretStoreの中身)
内部では、マネージドサービスに近い体験を、自ら制御できるコンポーネントで再現しています。
- 管理トークンの供給: Hubは更新可能なVault管理トークンをVault上の既知パスに保管し、ExternalSecret経由で取り込みます。HubのSidecarがTTL/2間隔で自動更新するため、通常運用で期限切れは起きません。
- Pre‑spawnによるユーザー分離: ユーザーサーバー起動時に、ユーザー専用ポリシーとそれに結びついたOrphanトークンを作成します。Orphanトークンは親トークンのポリシーに制約されず、継承関係の問題を回避できます。トークンは
NOTEBOOK_VAULT_TOKEN
としてノートブックに注入され、VAULT_ADDR
とともに利用されます。 - ユーザーごとの名前空間: ポリシーは各ユーザーの名前空間のみにアクセスを許可します。Vaultの監査ログによりアクセスは追跡可能です。
- ノートブック内ヘルパー:
buunstack
パッケージのSecretStore
が注入済みトークンを用いてVaultを呼び出します。各操作の前にlookup_self
でトークンを検査し、TTLが短ければ更新可能なトークンを自動更新します。無効になっている場合はエラーにし、ユーザーにサーバー再起動を促します。
ユーザー・トークンの更新は SecretStore._ensure_authenticated()
で実装されています。lookup_self
で現在のトークンを検査し、TTLが短く更新可能なら更新し、無効になっていればエラーにしてユーザーにサーバー再起動を促します。管理トークンの更新はHubのSidecarが別途処理し、ノートブックコードには関与しません。
この設計により、コード内でset/getするだけの手軽さと、ユーザー間の強力な分離、そして手動貼り付け不要の耐久的なセッションを両立できます。ポリシーやExternalSecret、ユーザーポリシーの範囲、Orphanトークン、更新挙動などの運用詳細は docs/jupyterhub.md
にまとめています。
JupyterHubをセルフホスト運用することの利点
JupyterHubを自前のKubernetesクラスタで運用すると、ノートブック体験の全体を制御でき、ローカルにもつことができます。利用するイメージやライブラリ、リソース配分、認証やシークレットの扱いを自分でカスタマイズできます。
- コントロールとカスタマイズ: チームの実態に合わせてイメージやプロファイルを作成し、Spawner・リソース・ストレージを調整
- データローカリティと性能: データをローカルネットワーク内に保ち、低レイテンシとシンプルなコンプライアンスを実現。CPU/メモリや GPU、ストレージをチューニング可能
- チーム生産性: 事前インストール済みツールで初期セットアップを短縮。ユーザーごとに再現性のある分離サーバー。クラスタ内DNSでサービスへ簡単に到達
- 運用とセキュリティ: KeycloakによるSSO、ユーザー分離、Vaultによる監査可能なシークレット管理、バックアップや監視の自前運用。長時間稼働が多い用途ではクラウドよりコストが押さえられ、予測しやすくなる
まとめ
本記事では、Kubernetes上のJupyterHubを実用的かつ安全に構築する方法を紹介しました。
- Just + Helmでのインストール(任意のNFS・Vault連携を含む)
- プロファイルとカスタムカーネルイメージ(Buun‑stack)の活用
- Kubernetes DNSと環境変数によるクラスタ内サービス(例: PostgreSQL)への接続
- Vaultと
SecretStore
によるノートブック内の安全なシークレット管理
SSO、クラスタ内連携、そして安全で使いやすいシークレット管理を備えた、自前運用のノートブック基盤として、小規模チームや学習用途、ホームラボに適しています。
参考
- リポジトリ: https://github.com/buun-ch/buun-stack
- ドキュメント:
docs/jupyterhub.md
- 関連投稿:
Discussion