Jupyter Lab on Kubernetesでの深層学習環境の構築
はじめに
今回はjupyter-labを用いてKubernetes(以下k8s)cluster上に深層学習環境を構築していきたいと思います.さらに,そのjupyter-labをL7ロードバランサ であるingress nginxで制御することによって,パスベースルーティングを行います.
パスベースルーティングを行うことで複数人がk8s clusterでjupyter-labを使用する際に非常に便利になります.
今回作成する環境は図1のようになります.
図1ではA・B・C三つのnamespaceに3つのjupyterをデプロイしていますが,以下ではnamespace tenzenにjupyterを1つデプロイします.個数の増やし方は名前やデプロイするnamespaceが変わるだけで他は全て同じ方法なので,各環境に合わせて個数を増減させてください.また,便宜上以下ではnamespace Aではなくnamespace tenzenとしてデプロイを行っています.
前回までのお話
以前Kubernetes(k8s) v1.16とNvidia-Docker2を用いたマルチノードDeepLearning環境の構築 Part2 - てんぜんの生存日誌では図2のような構成でk8s Cluster上に深層学習環境を構築しました.
記事を読んでもらえばわかるのですが,コンテナをroot権限で起動して,root権限でコンテナへsshして作業しています.root権限で制限なく作業できるのは便利なのですが,セキュリティを考えた時非常に危険です.また,k8s Master Nodeにsshした後Podにsshする多段sshの形になっているため非常に不格好なアクセス方法になってしまっています.
このままではコンテナ,k8sのいいところを生かし切れていないためこの方法を改善していきます.
Ingress Nginx
この節ではIngressリソースについて触れた後,マニフェストの準備を行い,実際にデプロイしていきます.
Ingressとは
複数人で使用することを想定しているため,Ingress Nginxでパスベースルーティングを行います.Ingressリソースの詳細については GitLab Runner on Kubernetesに敗北した話 - てんぜんの生存日誌を参照してください.
マニフェストの準備
今回はMetalLBとIngress Nginxの組み合わせで実装を行います.
まずはじめにIngressリソースで使用するロードバランサとしてMetalLBを準備します.下記のGItHubリポジトリからマニフェストをダウンロードしてきてください
- metallb.yaml
さらに,下記のようなMetalLB用のconfigMapである「metallb_confingmap.yaml」を作成してください. 「xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx」にはロードバランサで使用するIPアドレスの範囲を指定してください.
apiVersion: v1 kind: ConfigMap metadata: namespace: metallb-system name: config data: config: | address-pools: - name: metallb-ip-range protocol: layer2 addresses: - xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx
ロードバランサの準備ができたので,Ingress Nginxを準備していきます. 下記のGitHubリポジトリからIngress Nginx用のマニフェストを2つダウンロードしてきます.
- mandatory.yaml
- cloud-generic.yaml
ダウンロードしてきたマニフェストで変更する部分は特にありません.
デプロイ
先ほどダウンロードしてきたり作成したマニフェストをデプロイしていきます.
$ kubectl apply -f metallb.yaml $ kubectl apply -f metallb_confingmap.yaml $ kubectl apply -f mandatory.yaml $ kubectl apply -f cloud-generic.yaml
以上でIngress Nginxの準備は完了です.
Jupyter-Lab
この節ではJupyterについて軽く紹介した後,コンテナイメージの作成,マニフェストの準備,デプロイの順番で行っていきます.
Jupyterとは
JupyterはWEBアプリケーションとして動作するIDEのようなものです. 特徴としては,出力結果のグラフや画像をリアルタイムで表示させながらプログラミングをすることができる点です.そのためデータサイエンティストに人気があります.
Jupyterには主に三種類あります.
Jupyter-Notebook : 一番シンプルなJupyterで,基本的なプログラミング機能・画像やグラフの表示機能・MarkDownでのドキュメント記入機能などを有しています.
Jupyter-Lab : Jupyter-Notebookの拡張版で,Jupyter-Notebookで使用できる機能に加えて,ターミナルが使えたりフォルダ管理がGUIで行えたり,拡張パッケージで様々な新機能を追加できたりできます.
Jupyter-Hub : 複数のJupyter-NoteBookを制御して認証機能などをまとめて管理できます.
この3つを比較してみるとJupyter-Hubが非常に管理などにおいても優れているような印象を受けるかもしれませんが,下記の公式ドキュメントにある通りユーザがJupyterで使用するコンテナイメージはJupyter-Hub側であらかじめ指定したものの中からしか選べません.
zero-to-jupyterhub.readthedocs.io
そのためユーザが自由にコンテナイメージを選択したくても,ユーザ側の機能だけでは実現できません.今回は研究開発環境で使用する前提で環境構築を行っているので,ユーザ側で自由にコンテナイメージを選択もしくは自分で作成したDockerfileを元にビルドしたコンテナイメージを使用できないと使い物になりません.そこでそれぞれユーザのnamespaceを作成し,その中で別々にJupyter-Labサーバを作成する構成にしました.余談ですが,e-learningなどでユーザが利用するコンテナイメージが固定化されている場合は,Jupyter-Hubを使うのが管理面などから考えてもベストかもしれません.
コンテナイメージの作成
Jupyterに関する様々なイメージが以下のDockerHubのJupyter公式リポジトリで公開されています.
しかしながら上記イメージではGPUを使えなかったり,深層学習ライブラリであるKerasやPytorchを使用することができません.そのため,私が厳選した深層学習で必要なライブラリを詰め込んだオリジナルのDockerfileを作成してビルドします.
また,今回作成するコンテナイメージには以下のライブラリが含まれています.
- nvidia/cuda:10.0
- cudnn7
- curl
- git
- unzip
- imagemagick
- bzip2
- vim
- libsm6
- libgl1-mesa-dev
- build-essential
- libssl-dev
- pyenv
- nodesource
- node.js
- anaconda3-4.4.0
- opencv-python3.4.7.28
- tensorflow-gpu1.13.1
- addict
- keras
- torch
- torchvision
- progressbar
- jupyterlab2.0.0
- @lckr/jupyterlab_variableinspector
上記のパッケージをインストールする以下のようなDockerfileを作成してください.以下のDockerfileではroot権限でコンテナを作成しないようにするため,ユーザーを作成してユーザのhome以下に各種ライブラリをインストールしています.そのため「ENV USER_NAME tenzen」のところは自分の使用したいユーザ名に書き換えてください.
FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 LABEL maintainer="tenzen" ENV USER_NAME tenzen ENV UID 1000 RUN useradd -m -u ${UID} ${USER_NAME} ENV HOME /home/${USER_NAME} WORKDIR ${HOME} ENV PYENV_ROOT ${HOME}/.pyenv ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH RUN apt-get update \ && apt-get install -y curl \ git \ unzip \ imagemagick \ bzip2 \ vim \ libsm6 \ libgl1-mesa-dev \ build-essential \ libssl-dev \ && git clone https://github.com/pyenv/pyenv.git .pyenv \ && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && pyenv install anaconda3-4.4.0 \ && pyenv global anaconda3-4.4.0 \ && pyenv rehash \ && pip install --upgrade pip \ && pip install opencv-python==3.4.7.28 \ && pip install tensorflow-gpu==1.13.1 --ignore-installed --user \ && pip install addict \ && pip install keras \ && pip install torch \ && pip install torchvision \ && pip install tqdm \ && pip install torchsummary \ && pip install progressbar \ && pip install jupyterlab==2.0.0 \ && jupyter labextension install @lckr/jupyterlab_variableinspector \ && chown -R ${USER_NAME}: /home/${USER_NAME}/.local/
作成したDockerfileはビルドして自分が普段使用しているDocker Registryにpushして,Kubernetesで使用できるようにしてください.
マニフェストの準備
まずはじめ以前Rook CephFSでの障害ドメインのカスタマイズ - てんぜんの生存日誌で作成したRook CephFSを用いて,jupyterlab用の永続ボリュームをダイナミックプロビジョニングで作成します.以下のマニフェスト「pvc_tenzen.yaml」を参考に作成してください.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: tenzen-pvc namespace: tenzen spec: accessModes: - ReadWriteMany resources: requests: storage: 200Gi storageClassName: csi-cephfs
次に,下記のようなjupyterlab用のConfigmap・Service・Depoloymmentを含んだ「jupyterlab_deployment.yaml」を作成します.
kind: Service apiVersion: v1 metadata: name: jupyterlab-tenzen-clusterip namespace: tenzen spec: type: ClusterIP selector: app: jupyterlab-tenzen ports: - protocol: TCP port: 80 targetPort: 8888 --- apiVersion: v1 kind: ConfigMap metadata: name: jupyterlab-config namespace: tenzen data: jupyterlab-pass: "aiueo" jupyterlab-user-name: "tenzen" jupyterlab-user-id: "1000" jupyterlab-base-url: "/tenzen/" jupyterlab-home-dir: "/tenzen-pvc" --- apiVersion: apps/v1 kind: Deployment metadata: name: jupyterlab-deployment namespace: tenzen labels: app: jupyterlab-tenzen spec: replicas: 1 selector: matchLabels: app: jupyterlab-tenzen template: metadata: labels: app: jupyterlab-tenzen spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: hardware-type operator: In values: - "NVIDIAGPU" preferredDuringSchedulingIgnoredDuringExecution: - weight: 90 preference: matchExpressions: - key: gpu-type operator: In values: - "RTX2080Ti" securityContext: runAsNonRoot: true runAsUser: 1000 containers: - name: jupyterlab-tenzen image: hogehoge ports: - containerPort: 8888 env: - name: JUPYTERLAB_PASS valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-pass - name: UID valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-user-id - name: USER_NAME valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-user-name - name: BASE_URL valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-base-url - name: HOME_DIR valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-home-dir command: - "sh" - "-c" - | PASSWORD=$(python -c 'from notebook.auth import passwd;print(passwd("'${JUPYTERLAB_PASS}'"))') jupyter-lab --port=8888 --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.base_url=${BASE_URL} \ --NotebookApp.password=${PASSWORD} --NotebookApp.notebook_dir=${HOME_DIR} volumeMounts: - mountPath: "/tenzen-pvc" name: tenzen-pvc - mountPath: /dev/shm name: dshm resources: limits: nvidia.com/gpu: 2 cpu: 10000m memory: 64Gi volumes: - name: tenzen-pvc persistentVolumeClaim: claimName: tenzen-pvc readOnly: false - name: dshm emptyDir: medium: Memory
上記のマニフェストでわかりにくいところについて少し説明していきます.
- 以下のようなjupyterlabのdeployment用configmapでは環境変数として使用する7つのdataを設定しています.
- jupyterlab-pass : jupyterlabへ設定する認証用のパスワードを指定します.
- jupyterlab-user-name : jupyterlabコンテナ内で使用する一般ユーザの名前を設定しています.先ほどの作成したDockerfileで設定したユーザ名と同じものを設定してください.
- jupyterlab-base-url : jupyterlabをパスベースルーティングする際にパスを設定しています.
- jupyterlab-home-dir : jupyterlabの作業用ディレクトリを設定しています.例ではマウントしたPVCを指定しています.
apiVersion: v1 kind: ConfigMap metadata: name: jupyterlab-config namespace: tenzen data: jupyterlab-pass: "aiueo" jupyterlab-user-name: "tenzen" jupyterlab-user-id: "1000" jupyterlab-base-url: "/tenzen/" jupyterlab-home-dir: "/tenzen-pvc"
affinity.nodeAffinityにおいてpodをスケジューリングするNode選んでいますう.nodeselectorというフィールドも存在しますが,今回はより柔軟に設定できるAffinityを使用していきます.今回はNodeにあらかじめつけている「hardwaretype」と「gpu-type」の2つのラベルを用いてスケジューリングを行っています.まだラベルをつけていない方は以下のコマンドでラベルをつけてください.また例ではgpu-typeの方ではRTX2080Tiのラベルをつけていますが,使用環境で搭載されているGPUを設定してください.
- nodeへのラベル付与の方法
$ kubectl label node mynode hardwaretype=NVIDIAGPU $ kubectl label node mynode gputype=RTX2080Ti
spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution : これ以下にはスケジューリングの際必ず満たしてほしい条件を記述します.例ではhardwaretypeがNVIDIAGPUになっているNodeを選択しています.
spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution :これ以下にはスケジューリングの際できれば満たしてほしい条件を記述します.この項目には重みを設定でき,例では重み90に設定しています.また,条件としてはgpu-typeがRTX2080TiのNodeを選択しています.
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: hardware-type operator: In values: - "NVIDIAGPU" preferredDuringSchedulingIgnoredDuringExecution: - weight: 90 preference: matchExpressions: - key: gpu-type operator: In values: - "RTX2080Ti"
spec.template.spec.securityContextではroot権限ではなくDockerfileで指定した一般ユーザのUID:1000でコンテナを作成するように設定しています.また,spec.template.spec.containers.imageでは先ほど作成したDockerfileをビルドした後Container Registryへpush際つけた名前を指定してください.
- spec.template.spec.securityContext.runAsNonRoot : root権限でコンテナを作成するか選択できます.trueで非root権限,falseでroot権限になります.
- spec.template.spec.securityContext.runAsUser : 非root権限でコンテナを作成する場合に使用する一般ユーザのUIDを指定できます.
- "RTX2080Ti" securityContext: runAsNonRoot: true runAsUser: 1000 containers: - name: jupyterlab-tenzen image: hogehoge ports: - containerPort: 8888 env:
spec.template.spec.containers.envでは先ほどconfigmapで指定した値を読み込んでコンテナ内に環境変数として設定指定しています.
- containerPort: 8888 env: - name: JUPYTERLAB_PASS valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-pass - name: UID valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-user-id - name: USER_NAME valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-user-name - name: BASE_URL valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-base-url - name: HOME_DIR valueFrom: configMapKeyRef: name: jupyterlab-config key: jupyterlab-home-dir command:
spec.template.spec.containers.commandでシェルスクリプト実行してjupyterlabを起動しています.その際オプションとしてspec.template.spec.containers.envで環境変数として渡した値を使用しています.
- PASSWORD=$(python -c 'from notebook.auth import passwd;print(passwd("'${JUPYTERLAB_PASS}'"))') : pythonを起動してconfigmapで指定した値をハッシュ化してパスワードを作成しています.
- jupyter-lab : jupyterlabを起動します.
- --port=8888 : spec.template.spec.containers.ports.containerPortで開放したポートを指定しています.
- --ip=0.0.0.0 : 全てのIPアドレスを受け入れるようでアクセスできるように設定しています.
- --no-browser : 起動時にブラウザを起動しないように設定しています.
- --NotebookApp.token='' : tokenを使用しないようにシングルクォーテーションで囲むます.
- --NotebookApp.base_url=${BASE_URL} : configmapで指定したパスベースルーティングで使用するパスを環境変数から読み込んでいます.
- --NotebookApp.password=${PASSWORD} : 先ほどpythonで作成したパスワードを環境変数から読み込んでいます.
- --NotebookApp.notebook_dir=${HOME_DIR} : configmapで指定した作業用ディレクトリを環境変数から読み込んでいます.
key: jupyterlab-home-dir command: - "sh" - "-c" - | PASSWORD=$(python -c 'from notebook.auth import passwd;print(passwd("'${JUPYTERLAB_PASS}'"))') jupyter-lab --port=8888 --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.base_url=${BASE_URL} \ --NotebookApp.password=${PASSWORD} --NotebookApp.notebook_dir=${HOME_DIR} volumeMounts:
- 下記のIssueで報告されているようにPytorchなどで複数のGPUを使って学習を回すとshered memory(shm)が足りなくなって動きません.そこでshmを追加でマウントしてあげることでこの問題を解決します.
volumeMounts: ...(省略) - mountPath: /dev/shm name: dshm ...(省略) volumes: ...(省略) - name: dshm emptyDir: medium: Memory
デプロイ
上記で準備したマニフェストをデプロイしていきます.
$ kubectl apply -f pvc_tenzen.yaml $ kubectl apply -f jupyterlab_tenzen_deployment.yaml
以上でjupyterlabのマニフェスト準備は完了です.
L7ルーティング
この節では実際にどのようにパスベースルーティングを行っていくかについて説明した後,マニフェストの準備をしてデプロイしていきます.
概要
jupyterlabを各ユーザ用namespaceに閉じ込めているためこのままingressリソースを作成してもIngressとjupyterlabのserviceつまりclusteripと通信することができません.そこで下記issuesで提案されているServiceリソースのExternalNameを使ってnamespaceをまたいだ通信を行っていきます.
マニフェストの準備
以下のようなServiceリソースのExternalNameとingressリソースを含んだマニフェスト「jupyterlab_tenzen_ingress.yaml」を作成します.例では一つのパスしか用意していませんが,複数使う場合にはpathを追加していけばパス名でルーティングできます.注意点としてはhost名が必要ですので,DNSにhost名を登録しておいてください.簡易DNSについては GitLab Runner on Kubernetesに敗北した話 - てんぜんの生存日誌で紹介してるので,お困りの場合はそちらを参考にDNSを用意してください.
apiVersion: v1 kind: Service metadata: name: jupyter-tenzen-externalname namespace: ingress-nginx spec: type: ExternalName externalName: jupyterlab-tenzen-clusterip.tenzen.svc.cluster.local --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: jupyterlab namespace: ingress-nginx annotations: kubernetes.io/ingress.class: "nginx" ingress.kubernetes.io/secure-backends: "true" spec: rules: - host: jupyterlab.tenzen.local http: paths: - path: /tenzen backend: serviceName: jupyter-tenzen-externalname servicePort: 80
上記マニフェストでわかりにくいところをかいつまんで説明していきます.
metadata.namespaceへはingressリソースと同じものを設定する必要があります.また,spec.externalNameにはjupyterlabで使用しているServiceリソース,今回の場合clusteripの名前をSRVレコードを用いて指定する必要があります.
- spec.externalName : 宛先のServiceリソースを「[Service名].[Namespace名].svc.cluster.local」のテンプレートにしたがって記述してください.
apiVersion: v1 kind: Service metadata: name: jupyter-tenzen-externalname namespace: ingress-nginx spec: type: ExternalName externalName: jupyterlab-tenzen-clusterip.tenzen.svc.cluster.local
- spec.rules.http.paths.pathにはjupyterlabのconfigmapのjupyterlab-base-urlで指定した値を設定する必要があります.
spec: rules: - host: jupyterlab.tenzen.local http: paths: - path: /tenzen
以上でマニフェストの準備は完了です.
デプロイ
デプロイしていきます.
$ kubectl apply -f jupyterlab_tenzen_ingress.yaml
アクセス確認
ブラウザでIngressリソースのhost欄に指定したhost名とpathを使ってアクセスします.今回の例で言うと「http://jupyterlab.tenzen.local/tenzen」です.正常にアクセスされると以下のように表示されるともいます.
jupyterlab-configmapのjupyterlab-passで設定したパスワードを入力するとログインでき,以下のように表示されるはずです.
以上でGPU深層学習環境のJupyter Lab on KubernetesをIngress Nginxで制御することができました.
おわりに
今回はk8sの長所をできる限り使って深層学習環境を作成してみました.以前のみっともない多段SSHより格段によくなったのではないかと思っております.最後までお読みいただいてありがとうございました.またどこかでお会いしましょう.