kubeadmでGPU対応シングルノードクラスタを構築してOpenLDAPで認証するjupyterhubを立てる
これは MYJLab Advent Calendar 2022 Advent Calendar 2022 の24日目の記事です
(遅れてしまい本当に申し訳ありません)
前日の記事はこちらで
翌日の記事はこちらです
はじめに
このページでは
- kubeadm で GPU 対応のシングルノードクラスタを構築して
- jupyterhub サーバを立てて
- OpenLDAP で(jupyterhubの)認証
をします
上記のそれぞれは素晴らしい解説記事があり,Helm chart も用意されていてかなり楽ですが
通しでやるのはまた別の苦労があるためここに残します
(たぶんクリエイティブなことは特にありません)
できるもの(ざっくり)
Google Colaboratory 的なものができます,認証もかけます
GIF化したらとても遅くなったデモ動画です
背景
読まなくてもいい背景
- 研究室の共用マシンの管理がめんどくさい
- これまではLinuxユーザを作成してDockerの権限のみを付与して運用
- しかし機械学習(深層学習含む)用途がほとんど
- オレオレイメージでストレージを圧迫
- これまではLinuxユーザを作成してDockerの権限のみを付与して運用
- 使われていない高スペックマシンが他にもあるので,いずれはマルチノードにして処理時間を短縮したい
- 現状 Colab に対するアドバンテージは 設定の融通が効くことや時間制限がないことくらいで使う理由は特にないが,そのうち実験管理やMLOps的なことをしたい
前提
-
nvidia-smi
は実行可能な状態 -
alias k=kubectl
と設定 - マニフェストファイルはまとめることもできますが,基本的には分割
- 稼働しているクラスタでは検証したくないので,別のマシンで検証(下記)
$ screenfetch
./+o+- sh05@sh05gpu
yyyyy- -yyyyyy+ OS: Ubuntu 20.04 focal
://+//////-yyyyyyo Kernel: x86_64 Linux 5.4.0-135-generic
.++ .:/++++++/-.+sss/` Uptime: 3h 17m
.:++o: /++++++++/:--:/- Packages: 2649
o:+o+:++.`..```.-/oo+++++/ Shell: zsh 5.8
.:+o:+o/. `+sssoo+/ Disk: 76G / 116G (68%)
.++/+:+oo+o:` /sssooo. CPU: Intel Core i7-8750H @ 12x 4.1GHz [43.0°C]
/+++//+:`oo+o /::--:. GPU: NVIDIA GeForce GTX 1060
\+/+o+++`o++o ++////. RAM: 1986MiB / 32039MiB
.++.o+++oo+:` /dddhhh.
.+.o+oo:. `oddhhhh+
\+.++o+o``-````.:ohdhhhhh+
`:o+++ `ohhhhhhhhyo++os:
.o:`.syhhhhhhh/.oo++o`
/osyyyyyyo++ooo+++/
````` +oo+++o\:
`oo++.
kubeadmでGPU対応シングルノードクラスタを構築
gpu-operator-test (おためしPod)が動くまで
まずは kubeadm で Kubernetes クラスタを構築します,今回はシングルノードクラスタです
kubeadm は検証用途や小規模な Kubernetes クラスタを構築するためのツールで,
この目的ではデファクトな気がしています
より詳細な説明は README がわかりやすいと思います
コンポーネント自体はこっち
初めは,公式の構築ドキュメントや GPU 対応ドキュメントで進めていましたが
これら(↑)の内容をある程度内容を理解したら NVIDIA のドキュメントの方が進めやすかったです
(2023/10/17 追記) もしかしたら、リンク先が変わっているかも?
しかし,下記に注意して進める必要があります
不親切ですが,↓のドキュメント(再掲)と違いの出るコマンドだけ記載しますmm
-
タブは Docker ではなく containerd
-
クラスタを作成する際のコマンド
# sudo kubeadm init --pod-network-cidr=192.168.0.0/16
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
cf.
- Calico ではなく Flannel を利用
# kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
$ k apply -f https://raw.githubusercontent.com/flannel-io/flannel/v0.20.2/Documentation/kube-flannel.yml
- master から control-plane に変更
シングルノードクラスタなので master/control-plane にもスケジューリングされるように untaint するためのコマンドです
# kubectl taint nodes --all node-role.kubernetes.io/master-
$ kubectl taint nodes --all node-role.kubernetes.io/control-plane-
- nvidia-device-plugin は namespace を指定して helm install
# helm install --generate-name nvdp/nvidia-device-plugin
Error: INSTALLATION FAILED: execution error at (nvidia-device-plugin/templates/validation.yml:19:4):
Running in the 'default' namespace is not recommended.
Set 'allowDefaultNamespace=true' to bypass this error.
Otherwise, use --namespace (with --create-namespace as necessary) to run in a specific namespace.
See: https://helm.sh/docs/helm/helm_install/#options
$ helm install --generate-name nvdp/nvidia-device-plugin --create-namespace --namespace nvidia
確認コマンドでそれらしきログがでれば完了です
$ k logs gpu-operator-test
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done
MetalLBインストール
MetalLB をインストールします
これによってAWS や Google Cloud (名前変わった?)などのパブリッククラウドを
利用していると当然のように存在するtype: LoadBalancer
を使えるようになります
先ほど,kubeadm で推奨されている Calico ではなく Flannel を使用したのは
MetalLb と Calico の相性が悪そうだったためです
こちらもドキュメント通りでほとんど進みそうですが,
一部うまくいかないところがあるので,その辺りのみ記載します
(実は先ほどクラスタ構築で Helm をインストールしているので,Helm を使用します)
-
helm install
の前に,namespace を作成します
マニフェストを作成して
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
apply します
k apply -f namespace.yaml
- Secret も
helm install
前に作る
ないとPod が騒ぎ出して先に進まないので事前に作っておきます,この辺りを見ました
$ k create secret generic -n metallb-system metallb-memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
- 払いだすIPアドレスのPoolを指定
こちらを見て進めればよさそうで,一番単純な設定にします
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2ad
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
$ k apply -f ippool.yaml -f l2ad.yaml
確認コマンドはなんでもいいと思いますが,
例えば↓のコマンドで全て Runnning だといいと思います
$ k get po -A
kubectl 作業用のユーザ作成
管理者ユーザ kubectl を実行していると気持ちが悪いのでそろそろユーザを作成します
そのユーザに権限付与が必要ですが 考えるのがめんどくさいので ここでは管理者権限にします
こちらの記事の通りで間違いありませんでした
権限周りは独自の進め方をしたので,この流れのコマンドは全て載せます
$ openssl genrsa -out sh05.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
..............+++++
................................................................+++++
e is 65537 (0x010001)
# CNの部分がユーザ名だと思ってOK
$ openssl req -new -key sh05.key -out sh05.csr -subj "/CN=sh05"
$ cat sh05.csr | base64 | tr -d "\n"
--- 中身が出る ---
$ vim csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: sh05
spec:
request: --- 出た中身 ---
signerName: kubernetes.io/kube-apiserver-client
usages:
- client auth
$ k apply -f csr.yaml
certificatesigningrequest.certificates.k8s.io/sh05 created
$ k get csr
NAME AGE SIGNERNAME REQUESTOR REQUES
csr-g7c7w 28m kubernetes.io/kube-apiserver-client-kubelet system:node:sh05gpu <none>
sh05 7s kubernetes.io/kube-apiserver-client kubernetes-admin <none>
$ k certificate approve sh05
certificatesigningrequest.certificates.k8s.io/sh05 approved
$ k get csr
NAME AGE SIGNERNAME REQUESTOR REQUES
csr-g7c7w 28m kubernetes.io/kube-apiserver-client-kubelet system:node:sh05gpu <none>
sh05 32s kubernetes.io/kube-apiserver-client kubernetes-admin <none>
$ kubectl get csr sh05 -o jsonpath='{.status.certificate}'| base64 -d > sh05.crt
$ kubectl config set-credentials sh05 --client-key=sh05.key --client-certificate=sh05.crt --
User "sh05" set.
$ k config set-context sh05 --cluster=kubernetes --user=sh05
Context "sh05" created.
$ k config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kubernetes-admin@kubernetes kubernetes kubernetes-admin
sh05 kubernetes sh05
$ k config use-context sh05
Switched to context "sh05".
# 権限付与前なので権限不足になることを確認
$ k get po -A
Error from server (Forbidden): pods is forbidden: User "sh05" cannot list resource "pods" in
$ vim cluster-admin.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:masters
- apiGroup: rbac.authorization.k8s.io
kind: User
name: sh05
# 権限付与のために管理者にスイッチ
$ k config use-context kubernetes-admin@kubernetes
Switched to context "kubernetes-admin@kubernetes".
$ k apply -f cluster-admin.yaml
clusterrolebinding.rbac.authorization.k8s.io/cluster-admin configured
$ k config use-context sh05
Switched to context "sh05".
$ k get po -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-flannel kube-flannel-ds-xnspx 1/1 Running 0 32m
--- 割愛(表示できればOK) ---
jupyterhub 構築
本題の jupyterhub を構築します
といっても Kubernetes クラスタに関するドキュメントも豊富で Helm chart も用意してくれているので かなり楽です
今回はインストールと少しの設定だけします,ページ的にはここ
- インストール
$ 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 "metallb" chart repository
...Successfully got an update from the "nvdp" chart repository
...Successfully got an update from the "jupyterhub" chart repository
Update Complete. ⎈Happy Helming!⎈
$ helm upgrade --install jupyterhub jupyterhub/jupyterhub --create-namespace --namespace jhub
--- 割愛 ---
出力の中にPost-installation checklist
なるものがあるので従います
- Verify that created Pods enter a Running state:
kubectl --namespace=jhub get pod
If a pod is stuck with a Pending or ContainerCreating status, diagnose with:
kubectl --namespace=jhub describe pod <name of pod>
If a pod keeps restarting, diagnose with:
kubectl --namespace=jhub logs --previous <name of pod>
- Verify an external IP is provided for the k8s Service proxy-public.
kubectl --namespace=jhub get service proxy-public
If the external ip remains <pending>, diagnose with:
kubectl --namespace=jhub describe service proxy-public
- Verify web based access:
You have not configured a k8s Ingress resource so you need to access the k8s
Service proxy-public directly.
If your computer is outside the k8s cluster, you can port-forward traffic to
the k8s Service proxy-public with kubectl to access it from your
computer.
kubectl --namespace=jhub port-forward service/proxy-public 8080:http
まず Pod が Running になっているか
$ k get po -n jhub
NAME READY STATUS RESTARTS AGE
continuous-image-puller-zbkmr 1/1 Running 0 2m31s
hub-76b7b78bbd-l5n7n 0/1 Pending 0 2m31s
proxy-7c6cfb8b5-vf2b9 1/1 Running 0 2m31s
user-scheduler-9fc7fccb7-49vk8 1/1 Running 0 2m31s
user-scheduler-9fc7fccb7-zkzpm 1/1 Running 0 2m31s
なっていません,悲しいです
$ k describe po -n jhub hub-76b7b78bbd-l5n7n
-- 抜粋 --
Warning FailedScheduling 4m37s default-scheduler 0/1 nodes are available: 1 pod has unbound immediate PersistentVolumeClaims. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
とのことで,おそらく動的プロビジョニングが有効になっていないからだと思います
また,念の為に他も調べると迷子の PVC がいたり,values に与えることはほぼ必須だとわかったので
最終的に下記の yaml を作成します(過程などの詳細は追記予定です)
- config.yaml
- hdd01-pv.yaml
- hdd01-pvc.yaml
- hub-db-pv.yaml
今回は,ボリュームに関しては簡易的な対処をしています
spec.hostPath
を利用しているので
ノード上にディレクトリを作成して適切な権限にしておく必要があります
singleuser:
storage:
type: "static"
static:
pvcName: "hdd01-pvc"
subPath: 'home/{username}'
apiVersion: v1
kind: PersistentVolume
metadata:
name: hdd01-pv
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 500Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/hdd01/jupyterhub"
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: hdd01-pvc
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 450Gi
apiVersion: v1
kind: PersistentVolume
metadata:
name: hub-db-pv
labels:
component: hub
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
local:
path: /tmp
まとめて事前に apply し,helm upgrade にも設定ファイルを与えます
$ tree storage
storage
|-- hdd01-pvc.yaml
|-- hdd01-pv.yaml
`-- hub-db-pv.yaml
$ k apply -f storage -n jhub
$ helm upgrade --install jupyterhub jupyterhub/jupyterhub --namespace jhub --values config.yaml
--- 割愛 ---
$ k get po -n jhub
NAME READY STATUS RESTARTS AGE
continuous-image-puller-zbkmr 1/1 Running 0 37m
hub-7959954848-jphrg 1/1 Running 0 68s
proxy-76dd8b6d67-4cz2r 1/1 Running 0 68s
user-scheduler-9fc7fccb7-49vk8 1/1 Running 0 37m
user-scheduler-9fc7fccb7-zkzpm 1/1 Running 0 37m
この時点で下記のコマンドで出力される,IP アドレスにアクセスすると UI が表示され Jupyter Notebook を利用できます
$ k -n jhub get svc proxy-public --output jsonpath='{.status.loadBalancer.ingress[].ip}'
しかし helm show values ~~
してもわかる通り
hub.config.JupyterHub.authenticator_class.dummy
となっているため,何の認証認可もかかっていません,ザルです
そこで簡単に LDAP 認証をかけます
OpenLDAP で jupyterhub の認証
OpenLDAP導入
jupyterhub 側にもドキュメントがありますし
OpenLDAP にも Helm chart があります
最終的には下記の3ファイルを用意しました
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: data-myjlab-ldap-pvc
spec:
storageClassName: local-storage
accessModes:
- ReadWriteMany
resources:
requests:
storage: 8Gi
apiVersion: v1
kind: PersistentVolume
metadata:
name: data-myjlab-ldap-pv
labels:
type: local
spec:
storageClassName: local-storage
capacity:
# storage: 1Ti
storage: 10Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/hdd01/ldap"
global:
adminPassword: hogehoge
configPassword: fugafuga
imagePullSecrets:
- harbor
imageRegistry: ""
ldapDomain: myjlab.org
storageClass: ""
persistence:
accessModes:
- ReadWriteOnce
enabled: true
existingClaim: data-myjlab-ldap-pvc
size: 8Gi
storageClass: local-storage
$ tree storage
storage
|-- data-myjlab-ldap-pvc.yaml
`-- data-myjlab-ldap-pv.yaml
$ helm repo add helm-openldap https://jp-gouin.github.io/helm-openldap/
"helm-openldap" has been added to your repositories
$ helm install myjlab-ldap helm-openldap/openldap-stack-ha -f config.yaml --namespace=ldap --create-namespace
--- 割愛 ---
$ k apply -f storage/ -n ldap
--- 割愛 ---
$ helm upgrade myjlab-ldap helm-openldap/openldap-stack-ha -f config.yaml -n ldap
--- 割愛 ---
試しにデータを一つ作ります,この場だけなので phpldapadmin のサービスを nodePort に修正します
$ kubectl patch svc -n ldap myjlab-ldap-phpldapadmin --type json -p '[{"op": "replace", "path" : "/spec/type", "value": "NodePort"}]'
$ kubectl patch svc -n ldap myjlab-ldap-phpldapadmin --type json -p '[{"op": "replace", "path" : "/spec/ports/0/nodePort", "value": 31000}]'
$ echo "http://<nodeのhostname>:"`k get svc -n ldap myjlab-ldap-phpldapadmin -o jsonpath='{.spec.ports[0].nodePort}'`
# この出力にアクセス
とりあえず DN: cn=sh05,ou=doctor,dc=myjlab,dc=org
を作成
jupyterhub側修正
LDAP認証とその他プロファイル関連の設定をした最終的な config.yaml はこの通りです
ユーザの好きな Image で動かしてあげたかったところですが,本来の問題に戻ってしまうのでルート権限を付与しました
hub:
config:
Authenticator:
admin_users:
- sh05
allowed_users:
- test
JupyterHub:
authenticator_class: ldapauthenticator.LDAPAuthenticator
LDAPAuthenticator:
bind_dn_template:
- cn={username},ou=doctor,dc=myjlab,dc=org
server_address: myjlab-ldap.ldap.svc.cluster.local
singleuser:
uid: 0
fsGid: 0
storage:
type: "static"
static:
pvcName: "hdd01-pvc"
subPath: 'home/{username}'
profileList:
- display_name: "jupyterlab, GPU"
kubespawner_override:
extra_resource_limits:
nvidia.com/gpu: "2"
allowPrivilegeEscalation: true
uid: 0
gid: 0
args: ["--allow-root"]
defaultUrl: "/lab"
extraEnv:
JUPYTERHUB_SINGLEUSER_APP: "jupyter_server.serverapp.ServerApp"
- display_name: "jupyter notebook, GPU"
kubespawner_override:
extra_resource_limits:
nvidia.com/gpu: "2"
allowPrivilegeEscalation: true
uid: 0
gid: 0
args: ["--allow-root"]
- display_name: "jupyterlab, non GPU"
defaultUrl: "/lab"
extraEnv:
JUPYTERHUB_SINGLEUSER_APP: "jupyter_server.serverapp.ServerApp"
kubespawner_override:
allowPrivilegeEscalation: true
uid: 0
gid: 0
args: ["--allow-root"]
- display_name: "jupyter notebook, non GPU"
kubespawner_override:
allowPrivilegeEscalation: true
uid: 0
gid: 0
args: ["--allow-root"]
これを適用すれば初めのGIFのようになるかと思います~
主なTODO
- いろいろ実験管理しやすくする
- クラスタで何かがこけたらアラートさせたい
- (実はクラスタの監視システムも組んでるのでこの記事に載せようと思いましたが時間がなくて載せきれませんでした)
- MetalLB の Helm chart に prometheus-operator のカラムが見つかるのでそれを利用する
- (実はクラスタの監視システムも組んでるのでこの記事に載せようと思いましたが時間がなくて載せきれませんでした)
などなどです
Discussion