はじめに
こんにちは。SRE部MLOpsチームの中山(@civitaspo)です。みなさんはGWをどのように過ごされたでしょうか。私は実家に子どもたちを預けて夫婦でゆっくりする時間にしました。こんなに気軽に実家を頼りにできるのも全国在宅勤務制度のおかげで、実家がある福岡に住めているからです。「この会社に入って良かったなぁ」としみじみとした気持ちでGW明けの絶望と対峙しております。
現在、MLOpsチームでは増加するML案件への対応をスケールさせるため、Kubeflowを使ったMLOps基盤構築を進めています。本記事ではその基盤構築に至る背景とKubeflowの構築方法、および現在分かっている課題を共有します。
目次
MLOpsチームを取り巻く状況
冒頭で「増加するML案件への対応をスケールさせるため」と述べましたが、まずはその背景を私たちのチームが直面している状況を踏まえて説明します。
MLOpsチームは2019年4月に発足しました。当初はZOZO研究所がML機能開発を担当していたものの、プロダクションにその機能をリリースできていない課題がありました。その課題を解決するために、プロトタイプからプロダクションレベルへの引き上げをミッションとして発足したのがMLOpsチームです。このミッションは2年経った現在も変わっていません1。
ミッションは変わっていませんが、周囲を取り巻く環境は変わりました。なぜなら、着実にML機能のリリースを重ね、社内からの信頼度が高まってきているためです。これについて、もう少し深掘りして説明します。
私たちはチームの中長期目標として3つのフェーズを定めていました。
- Phase1: ML機能を1つ、プロダクションに出す
- Phase2: ML機能を複数、プロダクションに出す
- Phase3: ML機能の量産体制を整える
Phase1ではML機能を1つのプロダクションに出すことが目標でした。これは、私たちがレベルの高いインフラ、つまり技術選定が妥当である、安定している、十分に高速であるインフラを構築可能であると示すことで、MLOpsチームに対する社内からの信頼を獲得するための目標でした。同時に技術的なプレゼンスを高め、社外に対して発信することも目標に含んでいました。その最初のML機能が画像検索でした。
Phase2ではML機能を複数のプロダクションに出すことが目標でした。ここでは、Phase1で実践したレベルの高いインフラ構築、およびそれを用いたML機能のプロダクションリリースの再現性を示すことが重要でした。全ての事例は載せられないので、検索パーソナライズと推薦の代表的な2例を紹介します。
techblog.zozo.com techblog.zozo.com
そして、現在はPhase3の目標である、ML機能の量産体制の整備に取り組んでいます。Phase2までの取り組みで社内からの信頼を確実なものとしました。そのため、ML機能をリリースする案件も以前より増加しています。社外に対する技術的なプレゼンス向上も、実際に優秀な人材の採用に繋げられています。
しかし、案件の増加スピードに対して人材の増加が追いつかなくなる未来も見え始めています。そのため、ML機能のリリースをスケールさせるような基盤構築を進めています。この基盤が本記事で「MLOps基盤」と呼んでいるものです。複数のML機能をリリースしたことで、ML機能をプロダクションへリリースするために必要な共通要素・デザインパターンが分かってきました。その経験を元にMLエンジニアと協力して要件整理・検証を進めています。
MLOps基盤の要件
構築を進めているMLOps基盤の説明の前に、私たちの考えるMLOps基盤とは何かを説明します。
私たちの考えるMLOps基盤とは以下の要件を満たすものです。
- 運用中の予測モデル(ワークフロー)を一元管理できること
- モデル作成の際に環境構築が容易であること
- 実験段階からプロダクションへの移行が容易であること
- 車輪の再発明をしないような仕組みであること(= 似たようなモデル開発をしない)
- モデルサービングが可能であること
以前にAI Platform Pipelinesを取り上げた記事でも言及した内容ですが、改めて本記事でも説明します。
まず、「運用中の予測モデル(ワークフロー)を一元管理できる」必要があります。少人数で多数のML機能をリリースするためにはプロジェクト間・環境間の差分を極力排除し、構築・運用が共通化されていなければなりません。
同様の理由で「モデル作成の際に環境構築が容易である」ことも重要です。プロジェクト・環境が異なっても同じ方法で実験を開始できれば、その分だけMLエンジニアはモデル開発に集中できます。
さらに「実験段階からプロダクションへの移行が容易である」ことも必須です。実験段階のコードとプロダクションのコードが大幅に異なる場合、実験時と同じ結果を得られる保証がありません。そのため、再度検証が必要となり、大きな工数が必要となります。
「車輪の再発明をしないような仕組みである」ことはエンジニアなら当然考えることですが、MLOpsの文脈では過去の実験を再現可能であることが重要です。過去の実験をカタログのように扱い、新たなML機能をリリースする際にも過去の実験を参考・流用できる状態にしておく必要があります。
最後の「モデルサービングが可能である」ことは、モデルを構築すればそのままサービングが可能であることを求めています。モデルを構築しても別途サービング用のコードを書く必要がある場合、実装工数が必要となる他、学習時にオフライン評価で使用した推論結果とサービング時の推論結果が一致していることを保証する必要もあります。そのため、モデルサービングをフレームワークレベルでサポートし、MLエンジニアはモデル作成に専念できる状態を目指しています。
MLOps基盤技術としてのKubeflow
KubeflowはMLに必要な全てのワークロードをKubernetes上で実現するツールキットです。Kubeflowそのものに関しては先ほど紹介したAI Platform Pipelinesを取り上げた記事で説明しているので割愛します。Kubeflowに関する知見は既に社内で溜まりつつあり、またMLOps基盤としての要件を十分に満たす機能を持っていたため採用を決めました。
特に[Kubeflow Pipelines](https://www.kubeflow.org/docs/components/pipelines/overview/pipelines-overview/)の非常に高い実験管理機能は魅力的でした。Kubeflow Pipelinesはワークフローエンジンとして内部で[Argo Workflows](https://argoproj.github.io/projects/argo)を利用しています。Argo Workflowsではワークフローのタスク1つ1つがPodとなっているため、元データと使用するイメージに変更が無ければ多くのケースで何度でも同じ挙動を再現できます。Kubeflow Pipelinesではワークフローの実行ごとに、実行時メタデータだけでなくワークフローの定義自体も含めて保存しているため、過去の実行を容易に再現できます。 また、Kubeflowは[マルチテナンシーをサポート](https://www.kubeflow.org/docs/components/multi-tenancy/)しており、単一のKubeflowで複数のプロジェクトを管理できます。Kubeflow内部で[Profile](https://www.kubeflow.org/docs/components/multi-tenancy/design/)という単位で権限を管理できる機能を持っており、プロジェクト間で厳密な権限管理を行いつつ、Kubeflowという基盤に実験を集約することが可能です。1つの基盤を運用すれば良いので運用工数も大幅に削減できます。 そのため、MLOps基盤を構築する最初の目標としてマルチテナンシーが有効化されたKubeflowを構築し、Kubeflow Pipelinesを利用できる状態を目指しました。前置きが長くなりましたが、本記事ではこの目標を達成するためにKubeflowを構築した際に得られた知見、課題を共有します。 # なぜAI Platform Pipelinesを使わないのかKubeflow構築の説明をする前に、なぜAI Platform Pipelinesを使わなかったか触れておきます。MLOpsチームはGoogle Cloud Platform(以下、GCP)を使っているため、Kubeflow Pipelinesの代わりにGCPのマネージドサービスであるAI Platform Pipelinesを利用することも検討しました。しかし、複数の観点から採用を見送りました。
まず、AI Platform Pipelinesは1つのプロジェクトを作成する毎に1つのGoogle Kubernetes Engine(以下、GKE)が構築されてしまう点です。AI Platform Pipelinesは1つのGKEクラスタに複数構築することができないので、プロジェクトを増やす毎にGKEを構築する必要があります。GKEが増えれば増えるほど、GKEのバージョンアップ、監査ログ取得ツールFalcoなどの共通コンポーネントのインストール、などのクラスタ管理コストが増えてしまうため、運用がスケールしないと判断しました。
また、AI Platform Pipelinesの内部で保持するワークフローのデータなどをGKEに依存せず永続化するためにはCloud SQLを利用することになります。しかし、これに関してもプロジェクト増加毎に1インスタンス必要となりコスト面で許容できませんでした。Cloud SQLを使用しない場合は、GKE上にStatefulSetとしてMySQLがデプロイされ、Persistent Volumeに依存する構成となります。つまり、Zoneに依存する構成となり耐障害性が低くなってしまいます。
そして、一番課題と感じた点は利用者側でGKEにApplyされたManifestを直接書き換えても強制的に巻き戻ってしまう点です。問題発生時にManifestを修正することで問題解決できず、サポートケースを上げて解決することになるため、問題解決までのリードタイムが長くなってしまいます。
これらの理由によりMLOps基盤としてAI Platform Pipelinesの採用を見送りました2。
Kubeflowの構築
さて、Kubeflowを構築する話に移っていきます。なお、今回構築したKubeflowはv1.2.0で、GKE 1.18.16-gke.502を使用しています。
ドキュメント通りにKubeflowを構築する
最初にKubeflowの公式ドキュメントに沿ってKubeflow構築を進めました。このドキュメントに従うと、以下のように構成管理用GKEクラスタを使用して構築を進めることになります。
構成管理用GKEクラスタではConfig Connectorを有効化しています。Config ConnectorはKubernetesを介してGCPのリソース操作を可能にするGKEアドオンです。このアドオンをインストールするとKubernetesにGCPのリソースを定義するためのCustom Resource Definitionsが使用可能になります。例えば、以下のようなManifestをApplyするとsample-gcp-project
というGCP Projectにkubeflow-admin
という名称のService Accountが定義されます。
apiVersion: iam.cnrm.cloud.google.com/v1beta1 kind: IAMServiceAccount metadata: name: kubeflow-admin namespace: sample-gcp-project labels: kf-name: kubeflow spec: displayName: kubeflow admin service account
Kubeflowの公式ドキュメントでは、以下の手順でKubeflowを構築します。
- ①Config Connectorを有効化した構成管理用GKEクラスタを構築する
- ②Config Connectorを使用してGCPのリソースを作成する
- ③構築したGKE上へKubeflowに必要なManifest群をApplyする
これらの手順がMakefileに記述されており、make apply
で構築が完了するようになっています。
この手法は構成管理用GKEクラスタを構築する必要があるという点もさることながら、以下のような問題がありました。
- 既に構成管理に使用しているTerraformと役割が競合してしまう
- 既にManifest管理に使用しているKustomizeを使用できない
- Config Connectorで作成されるGCPリソースが私たちのインフラ要件を満たさない3
- 依存コンポーネントとしてMySQLやMinIOを利用するためPersistent Volumeに依存してしまう
これらの問題を解決しつつKubeflowを構築できるよう、次に示すような方法で構築しました。
Kubeflowを要件に合わせて構築する
私たちの環境に合わせた運用が可能になるよう、以下の方針でKubeflow構築を進めることにしました。
- Kubeflowの公式ドキュメントに沿ってManifest群の生成まで進める
- Config Connectorで定義されたGCPリソースはTerraformで管理可能なように移植する
- 出力したManifest群はKustomizeで必要なファイルのみ参照するようにし、必要に応じてPatchを当てる
まず、Kubeflowの公式ドキュメントに沿ってManifest群を生成します。
$ kpt pkg get https://github.com/kubeflow/gcp-blueprints.git/kubeflow@v1.2.0 kubeflow $ cd kubeflow $ make get-pkg
上記コマンドでManifest群生成に必要なファイルを準備し、可能な限り私たちのインフラ要件に合うように一部のファイルを修正します。
$ vim Makefile 55c55 < kpt cfg set ./instance gke.private false --- > kpt cfg set ./instance gke.private true # VPCネイティブクラスタで構築するため 57c57 < kpt cfg set ./instance mgmt-ctxt <YOUR_MANAGEMENT_CTXT> --- > kpt cfg set ./instance mgmt-ctxt null # 構成管理用GKEクラスタは利用しないため 59,62c59,62 < kpt cfg set ./upstream/manifests/gcp name <YOUR_KF_NAME> < kpt cfg set ./upstream/manifests/gcp gcloud.core.project <PROJECT_TO_DEPLOY_IN> < kpt cfg set ./upstream/manifests/gcp gcloud.compute.zone <ZONE> < kpt cfg set ./upstream/manifests/gcp location <REGION OR ZONE> --- > kpt cfg set ./upstream/manifests/gcp name kubeflow > kpt cfg set ./upstream/manifests/gcp gcloud.core.project sample-gcp-project > kpt cfg set ./upstream/manifests/gcp gcloud.compute.zone asia-northeast1 > kpt cfg set ./upstream/manifests/gcp location asia-northeast1 65,66c65,66 < kpt cfg set ./upstream/manifests/stacks/gcp name <YOUR_KF_NAME> < kpt cfg set ./upstream/manifests/stacks/gcp gcloud.core.project <PROJECT_TO_DEPLOY_IN> --- > kpt cfg set ./upstream/manifests/stacks/gcp name kubeflow > kpt cfg set ./upstream/manifests/stacks/gcp gcloud.core.project sample-gcp-project 68,71c68,71 < kpt cfg set ./instance name <YOUR_KF_NAME> < kpt cfg set ./instance location <YOUR_REGION or ZONE> < kpt cfg set ./instance gcloud.core.project <YOUR PROJECT> < kpt cfg set ./instance email <YOUR_EMAIL_ADDRESS> --- > kpt cfg set ./instance name kubeflow > kpt cfg set ./instance location asia-northeast1 > kpt cfg set ./instance gcloud.core.project sample-gcp-project > kpt cfg set ./instance email takahiro.nakayama@example.com $ vim instance/gcp_config/kustomization.yaml 13a14,16 > - ../../upstream/manifests/gcp/v2/privateGKE/ > patchesStrategicMerge: > - ../../upstream/manifests/gcp/v2/privateGKE/cluster-private-patch.yaml
修正が完了したらManifest群を生成します。
$ make set-values $ make clean-build $ make hydrate
これにより .build
ディレクトリ以下に大量のManifest群が生成されます。
$ find .build -type f | head -n10 .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_certificates.cert-manager.io.yaml .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_challenges.acme.cert-manager.io.yaml .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_orders.acme.cert-manager.io.yaml .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_issuers.cert-manager.io.yaml .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_certificaterequests.cert-manager.io.yaml .build/cert-manager-crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_clusterissuers.cert-manager.io.yaml .build/iap-ingress/networking.gke.io_v1beta1_managedcertificate_gke-certificate.yaml .build/iap-ingress/v1_configmap_ingress-bootstrap-config.yaml .build/iap-ingress/rbac.istio.io_v1alpha1_clusterrbacconfig_default.yaml .build/iap-ingress/cloud.google.com_v1beta1_backendconfig_iap-backendconfig.yaml $ find .build -type f | wc -l 505
これら全てのファイルを気合で読み進め、Terraform化、Kustomize化を進めます。
Config Connector用ManifestをTerraform化
ここまでの手順でConfig Connector向けのManifestも出力されるので、Terraform管理可能な定義に変換していきます。Config Connector向けのManifestは.build/gcp_config
以下のファイル群です。
これらファイル群で定義されているGCPリソースは以下の通りです。
- Virtual Private Cloud
- Static IP
- Persistent Disk
- Firewall rules
- Cloud Router
- Cloud NAT
- Cloud DNS
- Cloud IAM
- GKE
- 各種APIの有効化
GKEを構築済みである場合、Virtual Private CloudやCloud NATなどは既に存在しているはずなので、リソース作成の要不要は定義を読んで判断する必要があります。私たちの場合はFirewall rulesとCloud IAM以外は不要でした。
Manifest群をKustomizeで参照しPatchを当てる
生成したManifest群をKustomizeで参照するために、以下のようなディレクトリ構成をとることにしました。
. ├── generated │ └── kubeflow │ └── .build │ ├── application │ ├── cert-manager │ ├── cert-manager-crds │ ├── cert-manager-kube-system-resources │ ├── cloud-endpoints │ ├── gcp_config │ ├── iap-ingress │ ├── istio │ ├── knative │ ├── kubeflow-apps │ ├── kubeflow-issuer │ ├── metacontroller │ └── namespaces ├── base # generatedを参照する │ ├── application │ ├── cert-manager │ ├── cert-manager-leaderelection │ ├── cluster-resources │ ├── falco │ ├── iap-ingress │ ├── istio │ ├── knative │ ├── kubeflow-apps │ │ ├── argo │ │ ├── centraldashboard │ │ ├── jupyter-web-app │ │ ├── katib │ │ ├── kfserving │ │ ├── metadata │ │ ├── minio │ │ ├── ml-pipeline │ │ ├── notebook-controller │ │ ├── poddefaults │ │ ├── profiles │ │ ├── pytorch │ │ └── tfjob │ ├── kubeflow-issuer │ ├── kubeflow-istio │ ├── metacontroller │ └── nvidia-driver-installer ├── dev # baseを参照する ├── stg # baseを参照する └── prd # baseを参照する
generated/kubeflow/.build
以下のディレクトリに先ほど生成したManifest群が格納されています。生成したManifestへは直接変更を加えずbase
以下のディレクトリに格納するkustomization.yaml
から参照、Patchを加えます。
例えば、base/kubeflow-apps/argo/kustomization.yaml
で記述されているArgo Workflowsの設定は以下のようになります。
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: kubeflow resources: - ../../../generated/kubeflow/.build/kubeflow-apps/apiextensions.k8s.io_v1beta1_customresourcedefinition_workflows.argoproj.io.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/app.k8s.io_v1beta1_application_argo.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/apps_v1_deployment_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/apps_v1_deployment_workflow-controller.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/networking.istio.io_v1alpha3_virtualservice_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/rbac.authorization.k8s.io_v1beta1_clusterrole_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/rbac.authorization.k8s.io_v1beta1_clusterrole_argo.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/rbac.authorization.k8s.io_v1beta1_clusterrolebinding_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/rbac.authorization.k8s.io_v1beta1_clusterrolebinding_argo.yaml # NOTE: We use the configMapGenerator instead of these files. # - ../../../generated/kubeflow/.build/kubeflow-apps/v1_configmap_workflow-controller-configmap.yaml # - ../../../generated/kubeflow/.build/kubeflow-apps/v1_configmap_workflow-controller-parameters.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/v1_service_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/v1_serviceaccount_argo-ui.yaml - ../../../generated/kubeflow/.build/kubeflow-apps/v1_serviceaccount_argo.yaml configMapGenerator: # ref. https://github.com/argoproj/argo/blob/v2.3.0/docs/workflow-controller-configmap.yaml - name: argo-workflow-controller-config files: - config=config/workflow-controller.yaml configurations: - varReference.yaml vars: - name: ARGO_WORKFLOW_CONTROLLER_CONFIGMAP_NAME objref: kind: ConfigMap name: argo-workflow-controller-config apiVersion: v1 fieldref: fieldpath: metadata.name patchesStrategicMerge: - apps_v1_deployment_workflow-controller.yaml - v1_serviceaccount_argo.yaml
このような定義をKubeflowに含まれる全てのコンポーネントに行っていきます。そして、各環境用ディレクトリからbase
を参照する構成です。
先ほど課題に挙げていたPersistent Volumeへの依存もbase
でPatchを当てることで解消しました。
- MySQLをMySQL for Cloud SQLへ変更
- MinIOをMinIO GCS Gatewayへ変更
Kubeflowが公式に用意しているkubeflow/manifestsというリポジトリには様々なパターンへ対応するためのManifestが格納されています。そこに、MySQL for Cloud SQLやMinIO GCS Gatewayを利用するパターンも用意されていました4。
適切なNode Poolに配置する
ここまでの内容でKubeflowの構築が完了しました。構築に関する知見共有の最後にNode Poolの構成について触れておきます。
MLOps基盤ではKubeflowのController系Podを載せるNode Poolと、ワークフローのPodを載せるNode Poolを別々に管理する方針にしています。
KubeflowのController系Podを載せるNode Poolは、用途毎に占有のNode Poolを作成しました。用途以外のPodが配置されないようにtaint
を設定し、占有対象のPodが配置されるようにtolerations
とnodeAffinity
を設定します。
以下のようなPatchを定義し、kustomization.yaml
でPatchを当てます。
# dedicated-node-pool-patch.yaml - op: add path: /spec/template/spec/affinity value: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: cloud.google.com/gke-nodepool operator: In values: - kubeflow - op: add path: /spec/template/spec/tolerations value: - key: dedicated operator: Equal value: kubeflow effect: NoSchedule
# kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: kubeflow resources: - <...snip...> patches: - target: kind: StatefulSet path: dedicated-node-patch.yaml - target: kind: Deployment path: dedicated-node-patch.yaml
一方で、ワークフローのPodを載せるNode Poolは占有のNode Poolを作っていません。Node Auto Provisioningでワークロード毎にNode Poolを自動でプロビジョニングするようにしています。
なお、Node Auto ProvisioningはGKEのアドオンの1つです。このアドオンを有効化するとScheduleされたPodのResource Request、nodeAffinity
やlabelSelector
、taint
とtolerations
から最適な設定のNode Poolが自動で作成されます。MLOps基盤として利用者のワークフローがどれだけのリソースを必要とするのか事前に把握するのは困難であるため、Node Auto ProvisioningでオンデマンドにNode Poolが作成される構成としました。
Node Auto Provisioningの良いところは、GPUのプロビジョニングもサポートしているところです。利用者が必要なタイミングで何の相談も無くGPUが利用できる状態を作ることができます。
最終的には以下のようなNode Poolができています。nap-
から始まるNode PoolがNode Auto Provisioningによって生成されたNode Poolです。
運用課題
ここからは運用課題をいくつか紹介します。
Istioが古い
Kubeflowで利用されるIstioはv1.4です。Istioの最新バージョンはv1.9ですので非常に古いです。
また、v1.5でこれまでマイクロサービスとして存在していたコンポーネント群がistiodに統合される大きなアーキテクチャ変更がありました。そのため、現状のv1.4からバージョンが上がらないことに大きな危惧を感じています5。このGKEクラスタ上でサービングを始める前に解消されるべき課題です。
実は、Argo Workflowsも非常に古いバージョンである2.3.0(最新は3.0.0)を使用しています。Argo Workflowsに関しては、Kubeflow Pipelinesが依存しているのみなので、Istioほど大きな危惧は抱いていません。しかし、Kubeflowの依存コンポーネントがバージョンアップできない問題は今後も頭を悩ませ続けそうです。
kubernetes-sigs/applicationが異常な量のログを出力する
kubernetes-sigs/applicationはアプリケーションを構成する全てのコンポーネントを束ねて扱えるCustom Resource Definitionsを提供するプロジェクトです。Kubernetesで定義可能なDeploymentなどの単位ではアプリケーション全体を管理できないという課題から作られたようです。kubernetes-sigs/applicationはKubeflowの依存コンポーネントですが、構築直後からデフォルトで非常に大量のログを出力するようになっています。
私が構築したときは秒間1000件以上のログを出力していました。GKE上でこの量のログが出力されるとCloud Loggingのコストが高額になってしまいます。この問題はKubeflow側でも認識されていて、Issue(Stackdriver Logs are very expensive for kubeflow - kubeflow/gcp-blueprints#184)になっています。その、kubeflow/gcp-blueprints#184ではkubernetes-sigs/applicationのログを全て/dev/null
に捨てるという豪快なアプローチで解決が図られています。しかし、私たちはkubernetes-sigs/applicationを削除することにしました。なぜなら、kubernetes-sigs/applicationが存在しなければ動かないコンポーネントがKubeflowに存在しないからです。
Kubeflow PipelinesとKubernetesの不整合
Kubeflow Pipelinesは自身のDBに持つ状態を正として扱います。
一方、Kubernetes上の状態がKubeflow Pipelinesの持つ情報と異なっていても、Kubernetes上の状態を修正しません。また、Kubeflow PipelinesのUIからはDBに格納されている情報が表示されるのみで、その不整合状態を確認できません。そのため、Kubeflow Pipelinesの持つ情報とKubernetes上の状態との差異が発生すると、実際の状態を誤認してしまいます。
そして、この不整合状態は比較的高い確率で起こることが確認できています。原因が不明なものもあるため、確実に原因が分かっている2つのケースを紹介します。
1つ目はKubeflow Pipelinesによって作成されたObjectを削除するケースです。このケースは手動運用が禁じられている本番環境では起きえないので深く考える必要はありません。
もう1つはOwnerReferenceによって親Objectと共にObjectが削除されてしまうケースです。分かりにくいと思うので図を用いて説明します。
Kubeflow PipelinesではSchedule実行のためにRecurring Runという機能があります。Recurring Runは時間になったらRunという機能でワークフローを実行します。Recurring RunとRunはKubernetes上でOwnerReferenceによって親子関係ができています。そのため、Runを実行中にRecurring Runを削除した場合、Runも一緒に削除されてしまいます。
しかしながら、Kubeflow Pipelines上で明示的に削除が行われたわけではないため、Kubeflow PipelinesのUIではRunは実行中ステータスのままになってしまうのです。
非常に危険な問題なので、Kubeflow Pipelinesの保持する状態とKubernetes上の状態を比較、監視する仕組みを導入しようと思っています。
最後に
本記事では現在構築中のMLOps基盤を紹介しました。記事内で取り上げた課題は解決に向けて絶賛取り組んでいるところです。特にIstioの最新化は急ピッチで進めています。また、インフラ部分だけではなく、MLOps基盤としてMLエンジニアをサポートする機能強化を実施していきたいと思っています。折を見て記事を書きますので期待して待っていて頂けると嬉しい限りです。
本記事に載せた内容以外にも様々な観点で機能を検証追加しています。絶賛構築中なので、関心を持たれた方は1度お話しをさせてもらえるとありがたいです。是非助けてください!
ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!
https://tech.zozo.com/recruit/tech.zozo.com hrmos.co
- ミッションや文化に関する詳しい説明は前リーダーである@sonotsさんが『ZOZO MLOps のチームリーディングとSRE(Engineering)』で語っています。興味のある方はご覧ください。↩
- 2021-05-19にAI PlatformがVertex AIという名前となりPipelinesからGKE依存がなくなったので再検討の余地があります。↩
- 例えば私たちは『GCP Shared VPCを利用した全社共通ネットワークの運用におけるDedicated Interconnect利用設定の最適化手法』で説明したようにShared VPCを使用しています。そのためサービスプロジェクト側からホストプロジェクト側のFirewall rulesを操作することを認めていません。↩
- 実は私たちのMySQL for Cloud SQLはPrivate Service Accessを構成しています。そのためCloud SQL Auth Proxyを使う構成では無く、単に接続情報を変更するだけで済みました。↩
- issueは存在しています。↩