Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

EKSにJupyterHubを構築した

DRE&MLOpsチームの會田@ryoaitaです。この記事はGunosy Advent Calendar 2021の24日目の記事です。昨日の記事は今村さんの「こんな使い方もできるよgqlgen」でした。前期より広告技術部GunosyAdsチームからDRE&MLOPsチームに異動しました。MLエンジニア向けの実験環境としてJupyterHubをKubernetes上に構築したなかで、いくつか躓いたところがあったので、他の方に参考になればとおもい構築した内容の一部を紹介します。

JupyterHub

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_resourcelocal-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の機能を触ることになり勉強になりました。この記事が少しでも参考になれば嬉しいです。