😭

kubeadmでGPU対応シングルノードクラスタを構築してOpenLDAPで認証するjupyterhubを立てる

2022/12/25に公開

これは MYJLab Advent Calendar 2022 Advent Calendar 2022 の24日目の記事です
(遅れてしまい本当に申し訳ありません)

前日の記事はこちらで

翌日の記事はこちらです

はじめに

このページでは

  1. kubeadm で GPU 対応のシングルノードクラスタを構築して
  2. jupyterhub サーバを立てて
  3. OpenLDAP で(jupyterhubの)認証

をします
上記のそれぞれは素晴らしい解説記事があり,Helm chart も用意されていてかなり楽ですが
通しでやるのはまた別の苦労があるためここに残します
(たぶんクリエイティブなことは特にありません)

できるもの(ざっくり)

Google Colaboratory 的なものができます,認証もかけます

GIF化したらとても遅くなったデモ動画です

背景

読まなくてもいい背景
  • 研究室の共用マシンの管理がめんどくさい
    • これまでは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 追記) もしかしたら、リンク先が変わっているかも?
https://gitlab.com/nvidia/kubernetes/device-plugin#quick-start

しかし,下記に注意して進める必要があります

不親切ですが,↓のドキュメント(再掲)と違いの出るコマンドだけ記載しますmm

  1. タブは Docker ではなく containerd

  2. クラスタを作成する際のコマンド

# sudo kubeadm init --pod-network-cidr=192.168.0.0/16
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

cf.

  1. 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
  1. 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-
  1. 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 を使用します)

  1. helm installの前に,namespace を作成します
    マニフェストを作成して
namespace.yaml
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
  1. Secret もhelm install前に作る

ないとPod が騒ぎ出して先に進まないので事前に作っておきます,この辺りを見ました

$ k create secret generic -n metallb-system metallb-memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
  1. 払いだすIPアドレスのPoolを指定

こちらを見て進めればよさそうで,一番単純な設定にします

ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.200-192.168.1.250
l2ad.yaml
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
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
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 も用意してくれているので かなり楽です

今回はインストールと少しの設定だけします,ページ的にはここ

  1. インストール
$ 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を利用しているので
ノード上にディレクトリを作成して適切な権限にしておく必要があります

config.yaml
singleuser:
  storage:
    type: "static"
    static:
      pvcName: "hdd01-pvc"
      subPath: 'home/{username}'
hdd01-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: hdd01-pv
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 500Gi
  accessModes:
  - ReadWriteMany
  hostPath:
    path: "/hdd01/jupyterhub"
hdd01-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: hdd01-pvc
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 450Gi
hub-db-pv.yaml
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ファイルを用意しました

data-myjlab-ldap-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: data-myjlab-ldap-pvc
spec:
  storageClassName: local-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 8Gi
data-myjlab-ldap-pv.yaml
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"
config.yaml
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 で動かしてあげたかったところですが,本来の問題に戻ってしまうのでルート権限を付与しました

config.yaml
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