DRE&MLOpsチームの會田@ryoaitaです。この記事はGunosy Advent Calendar 2021の24日目の記事です。昨日の記事は今村さんの「こんな使い方もできるよgqlgen」でした。前期より広告技術部GunosyAdsチームからDRE&MLOPsチームに異動しました。MLエンジニア向けの実験環境としてJupyterHubをKubernetes上に構築したなかで、いくつか躓いたところがあったので、他の方に参考になればとおもい構築した内容の一部を紹介します。
JupyterHub
JupyterHubはマルチユーザーでJupyter Notebookを利用するためのWebアプリケーションです。JupyterHubを利用すると、ユーザー毎に独立したJupyter Notebookサーバーを提供することができます。
JupyterHubを利用しやすいMLエンジニア向けの実験環境として社内に提供するために、JupyterHubをEKSに構築しました。
Zero to JupyterHub with Kubernetes
Zero to JupyterHub with Kubernetes はJupyterプロジェクトから公式に提供されている、Kubernetesのデプロイや運用方法をまとめたドキュメントです。また、JupyterHubをKubernetesにデプロイするためのHelmのチャートも提供されています。
Helmのチャートの利用
私もこの提供されているチャートを利用してJupyterHubを構築しました。しかし、 external-secrets から作成したSecretやEFSのPersistent VolumeなどのJupyterHubのチャートの外で作成・管理したいKubernetesのオブジェクトがある場合にはこのチャートのリリースを作成するだけでは構築出来ません。
そこで、JupyterHubのチャートをサブチャートとするプロジェクトのチャートを別に作成して、プロジェクトのチャートで作成したオブジェクトの名前がJupyterHubのチャートのテンプレートに渡るようにしました。
そのためには、まずChart.yamlのdependenciesにJupyterHubのチャートを追加します。
dependencies: - name: jupyterhub version: 1.2.0 repository: https://jupyterhub.github.io/helm-chart
サブチャートのvaluesは、 <サブチャートの名前>.<キー名>
の形式でvaluesに指定します。
_helpers.tpl に次のように、valuesからリソースの名前を指定するためのヘルパーを追加します。
{{/* Create chart name of the external secret for hub */}} {{- define "test-jupyterhub.hubExternalSecretName" -}} {{- .Values.jupyterhub.hub.existingSecret }} {{- end }}
そして、ExternalSecretなどのテンプレートでヘルパーを使用してリソースの名前を指定します。
apiVersion: 'kubernetes-client.io/v1' kind: ExternalSecret metadata: name: "{{ include "test-jupyterhub.hubExternalSecretName" . }}" labels: {{ include "test-jupyterhub.labels" . | indent 4 }} spec: backendType: secretsManager data: - key: {{ .Values.externalSecrets.postgres | quote }} name: "hub.db.password" property: password --- apiVersion: 'kubernetes-client.io/v1' kind: ExternalSecret metadata: name: "{{ include "test-jupyterhub.sshKeysExternalSecretName" . }}" labels: {{ include "test-jupyterhub.labels" . | indent 4 }} spec: backendType: secretsManager data: - key: {{ .Values.externalSecrets.sshKeys | quote }} name: id_rsa property: private_key
こうすることによって、ExternalSecretのカスタムリソースのようなJupyterHubのチャートでは管理されていないものも、Helmで管理できるようになります。
Dockerイメージ
Jupyter NotebookのためのDockerイメージの作成には、 Jupyter Docker Stacks で提供されているDockerイメージをベースにしました。
これは jupyter/base-notebook に含まれているシェルスクリプトをPodの起動時に使用するためです。 このシェルスクリプトはJupyter Notebookサーバーの起動時に便利なオプションが含まれています。
Jupyter NotebookのDockerイメージの作成方法には他に repo2docker を利用する方法も有りますが、repo2dockerで作成したイメージには前述のシェルスクリプトが含まれないため採用を見送りました。
Taints と Tolerations
計算用のJupyter Notebookサーバーには多くのCPUやメモリを割り当てるため、ホストとなるEC2インスタンスでは他よりも強力なインスタンスタイプを利用するため、Kubernetesクラスタ上の他のシステムとはNodeをわけるようにしました。
JupyterNotebookサーバ用のNodeとそれ以外のNodeを分ける場合、Taints と Tolerationsを利用します。Helmのvaluesには次の指定を追加します。
scheduling: userPods: nodeAffinity: # matchNodePurpose valid options: # - ignore # - prefer (the default) # - require matchNodePurpose: require
JupyterHubのHelmのチャートにはあらかじめ計算用のNodeを分けるためのTaintsとTolerationsのための設定がされているので、こちらのドキュメントの記述に従って、NodeにTaintsの指定をします。
EKSでCluster Autoscalerを利用して、Nodeに割り当てるインスタンスをAuto Scaling Groupで管理している場合、NodeにTaintsを指定するには、Auto Scaling Groupにタグを設定する必要があります。
Auto Scaling GroupのタグはAWSリソースを管理しているTerraformで追加しました。
EKSのマネージドノードを管理している aws_eks_node_group
からはAuto Scaling Groupを直接操作することできないので、 null_resource
の local-exec
を使用して、AWSコマンドを実行します。
resource "null_resource" "add-custom-tags-to-asg" { for_each = local.eks_node_group_attributes triggers = each.value.asg_tags provisioner "local-exec" { command = <<EOF %{for key, value in each.value.asg_tags~} aws autoscaling create-or-update-tags --profile $AWS_PROFILE_FOR_LOCAL_EXEC \ --tags ResourceId=${aws_eks_node_group.eks-node-group[each.key].resources.0.autoscaling_groups.0.name},ResourceType=auto-scaling-group,Key=${key},Value=${value},PropagateAtLaunch=true %{endfor~} EOF } depends_on = [ aws_eks_node_group.eks-node-group, ] }
終わりに
前期の施策の一つとしてDRE&MLOpsではEKSクラスタにJupyterHubを構築しました。 まだ構築したばかりで、現在は日頃の実験をJupyterHub上で行えるように、AdsMLチームに協力してもらい、モデルの学習のコードの修正やその他の改善に努めています。
JupyterHubは公式のドキュメントやHelmのチャートのおかげで構築しやすいですが、TaintsとTorerationなどこれまでには利用したことのないKubernetesの機能を触ることになり勉強になりました。この記事が少しでも参考になれば嬉しいです。