Evidently AI 公開サンプルをKubernetes上で実行






<!--
title: Evidently AI 公開サンプルをKubernetes上で実行
tags: Kubernetes, MLOps, モニタリング, Evidently AI
-->

# はじめに
MLOpsを構成する機能要素としてモニタリングがあります。推論精度の変化やデータのドリフトの発生を捉えてモデルの再学習や再開発に繋げる役割を担うことになる重要な要素と考えています。このモニタリングを果たすOSSとして[Evidently AI](https://evidentlyai.com/)があり、様々なメトリクスを算出してレポートを作成できるライブラリがあります。  
Evidently AIの使い方やサンプルは[GitHub](https://github.com/evidentlyai/evidently)で公開されています。この中にはメトリクスの状況をPrometheusとGrafanaを利用してリアルタイムに可視化する[Real-time ML monitoring with Evidently and Grafana](https://github.com/evidentlyai/evidently/tree/main/examples/integrations/grafana_monitoring_service)というサンプルがあります。この公開サンプルはdocker composeで実行するものになっていますが、今回はこれをKubernetes(minikube)上で動かすことにチャレンジしてみました。

# システム環境
- PC: Windows 11
- WSL2: Ubuntu 20.04
- Docker 20.10.17
- Python 3.8.10
- Amazon Elastic Container Registry (ECR)
    - AWS CLIがインストールされていることが必要です。

今回はローカルPC(Windows)にWSL2+Docker(Ubuntu 20.04)の環境を用意して、そこでminikubeを動かすことにしました。また、Dockerイメージの管理のためにAWSのECRを使いました。ですが、EKSなど他のKubernetes環境でも動作すると思いますし、DockerのコンテナレジストリもECR以外のものであっても動作するかと思います。

全体の構成としては以下のようになります。
![sub003-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/3059ff65-a833-bf43-f88a-78c93d21770f.png)
- この公式サンプルでは機械学習の推論を行っていません。推論実行済のデータ(productionデータ)を定期的に転送しています。
- Evidently AIでモニタリングする際はモデル学習時のデータ(referenceデータ)が必要になります。これも事前に用意したデータを読み取る形にしています。

また、フォルダ構成は以下のようになります。

![sub003-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/bd3c7de8-a679-aea7-4df2-0f5b6b3524c7.png)

# 構築手順
## 1. 環境準備
Minikubeおよび他のツール群をインストールします。インストール方法はいくつかありますが、今回は`asdf`というminikube、kubectlなどのバージョン管理が行えるツールを使うことにしました。
```sh
# asdfのインストール
git clone https://github.com/asdf-vm/asdf.git ~/.asdf
cat <<EOF >> ~/.bashrc
# asdf setting
. $HOME/.asdf/asdf.sh
. $HOME/.asdf/completions/asdf.bash
EOF
exec $SHELL -l

# minikubeのインストール
# 今回はバージョンとして1.23.2を指定
asdf plugin-add minikube
asdf list-all minikube   # minikubeのバージョン一覧が出てくる
asdf install minikube 1.23.2
asdf global minikube 1.23.2

# kubectlのインストール
# 今回はバージョンとして1.23.2を指定
asdf plugin-add kubectl
asdf list-all kubectl   # kubectlのバージョン一覧が出てくる
asdf install kubectl 1.23.2
asdf global kubectl 1.23.2
```

minikubeを起動します。
```sh
minikube start --kubernetes-version=v1.23.2 --memory=6g
```

minikubeからECRにアクセスするための認証設定を行います。
```sh
# 認証情報の設定
minikube addons configure registry-creds
```
上記コマンドを実行すると対話形式で入力を促されます。以下のように入力します。
最後に`registry-creds was successfully configured`と表示されればOKです。
```
Do you want to enable AWS Elastic Container Registry? [y/n]: y
-- Enter AWS Access Key ID: <アクセスキー>
-- Enter AWS Secret Access Key: <シークレットアクセスキー>
-- (Optional) Enter AWS Session Token: <空でEnter>
-- Enter AWS Region: <リージョン>
-- Enter 12 digit AWS Account ID (Comma separated list): <AWSアカウントID>
-- (Optional) Enter ARN of AWS role to assume: <空でEnter>

Do you want to enable Google Container Registry? [y/n]: n

Do you want to enable Docker Registry? [y/n]: n

Do you want to enable Azure Container Registry? [y/n]: n
```
入力が終わったら認証設定の有効化を行います。
```sh
# 認証設定の有効化
minikube addons enable registry-creds
```

GitHubからEvidently AI 公開サンプルをクローンします。
```sh
git clone https://github.com/evidentlyai/evidently.git
```

## 2. Prometheus/Grafanaで利用される設定ファイルの作成
公開サンプルの中で使用される設定ファイルは、クローンしたリポジトリの`evidently/examples/integrations/grafana_monitoring_service/config/`フォルダにyaml形式で格納されています。
- grafana_dashboards.yaml
- grafana_datasources.yaml
- prometheus.yml

今回はこれをConfigMapとして定義します。一部の設定を今回の環境に合わせて変更しています。また元のyamlファイルあったコメントは除外しています。

まずはマニフェストファイルを格納するフォルダを作成します。
```sh
mkdir manifests
```

次に各マニフェストファイルを作成します。
```yaml: manifests/cm_grafana-dashboards.yaml (grafana_dashboards.yamlのConfigMap定義)
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-grafana-dashboards
data:
  grafana-dashboards.yaml: |
    apiVersion: 1
    providers:
      - name: 'Evidently Dashboards'
        orgId: 1
        folder: ''
        folderUid: ''
        type: file
        disableDeletion: false
        updateIntervalSeconds: 10
        allowUiUpdates: false
        options:
          path: /opt/grafana/dashboards
          foldersFromFilesStructure: true
```
```yaml: manifests/cm_grafana-datasource.yaml (grafana_datasources.yamlのConfigMap定義)
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-grafana-datasources
data:
  grafana-datasources.yaml: |
    apiVersion: 1
    datasources:
      - name: Prometheus
        type: prometheus
        access: proxy
        url: http://localhost:9090
        isDefault: true
```
【変更箇所】
- url： `http://prometheus.:9090` -> `http://localhost:9090`

```yaml: manifests/cm_prometheus.yaml (prometheus.ymlのConfigMap定義)
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-prometheus
data:
  prometheus.yml: |
    global:
      scrape_interval:     15s
      evaluation_interval: 15s
      external_labels:
          monitor: 'my-project'
    scrape_configs:
      - job_name: 'prometheus'
        scrape_interval: 5s
        static_configs:
            - targets: ['localhost:9090']
      - job_name: 'service'
        scrape_interval: 10s
        static_configs:
        - targets: ['ai-monitor-clusterip:8085']
```
【変更箇所】
- alertingの設定は今回利用しないので削除
- job_name: 'service' の targets: `['evidently_service.:8085']` -> `['ai-monitor-clusterip:8085']`

## 3. AIモニタのDockerイメージのビルド、およびECRへのプッシュ
次にEvidently AIが動作するAIモニタのDockerイメージをビルドします。最初にECRにリポジトリを用意しておき、ビルド＆プッシュします。Dockerイメージを作成するために必要なDockerfileはEvidently AI 公開サンプルをそのまま利用します。
```sh
# AWSアカウントとリージョンの定義
# ご自身の環境に合わせて適宜修正してください。xxxxxxxxxxxxは12桁のAWSアカウントID
AWS_ACCOUNT=xxxxxxxxxxxx
AWS_REGION=ap-northeast-1

# ECRにログイン
# 下記コマンドをこのまま実行（--usernameを書き換えたりする必要なし）
# 認証の有効期限は12時間。それ以上経過した場合は再度コマンドを実行してください。
aws ecr get-login-password --region $AWS_REGION | \
 docker login --username AWS \
   --password-stdin $AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com

# ECRへリポジトリの作成
# `ai-monitor`というリポジトリにします。
aws ecr create-repository --repository-name ai-monitor

# Evidently AIサンプルのフォルダに移動
cd evidently/examples/integrations/grafana_monitoring_service

# ai-monitorイメージの作成
# dockerイメージ名はリポジトリ名と同じである必要があります。
docker build -t $AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/ai-monitor .

# イメージのプッシュ
docker push $AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/ai-monitor

# ホームディレクトリに戻る
cd ~
```

## 4. Kubernetesのマニフェストファイルの作成
Kubernetesの各種リソース（Pod, Deployment, Service）を作成するために、マニフェストを作成します。
- prometheus/grafana用のリソースのマニフェスト
    - prometheus/grafanaの設定情報はConfigMapで定義したものを使用します。
    - grafanaのダッシュボード定義は、jsonファイルをvolumeから読み取る形にしています。
```yaml: manifests/depsvc_prometheus-grafana.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: prometheus-grafana
  name: prometheus-grafana
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus-grafana
  template:
    metadata:
      labels:
        app: prometheus-grafana
    spec:
      containers:
      - image: prom/prometheus
        name: prometheus
        args: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.path=/prometheus"]
        ports:
        - containerPort: 9090
        volumeMounts:
          - name: prometheus-config
            mountPath: /etc/prometheus
      - image: grafana/grafana
        name: grafana
        securityContext:
          runAsUser: 472
        ports:
        - containerPort: 3000
        volumeMounts:
          - name: grafana-datasources
            mountPath: /etc/grafana/provisioning/datasources
            readOnly: true
          - name: grafana-dashboards
            mountPath: /etc/grafana/provisioning/dashboards
            readOnly: true
          - name: dashboards
            mountPath: /opt/grafana/dashboards
      volumes:
        - name: prometheus-config
          configMap:
            name: cm-prometheus
        - name: grafana-datasources
          configMap:
            name: cm-grafana-datasources
        - name: grafana-dashboards
          configMap:
            name: cm-grafana-dashboards
        - name: dashboards
          hostPath:
            path: /data/grafana_monitoring_service/grafana-dashboards
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: prometheus-grafana-clusterip
  name: prometheus-grafana-clusterip
spec:
  ports:
  - name: prometheus
    port: 9090
    protocol: TCP
    targetPort: 9090
  - name: grafana
    port: 3000
    protocol: TCP
    targetPort: 3000
  selector:
    app: prometheus-grafana
  type: ClusterIP
```
- ai-monitor(Evidently AI)用のリソースのマニフェスト
    - referenceデータはvolumeから読み取る形にしています。
    - xxxxxxxxxxxx部分は各自のAWSアカウントIDを設定します。（リージョンも異なれば合わせて修正してください。）
```yaml: manifests/depsvc_ai-monitor.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ai-monitor
  name: ai-monitor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ai-monitor
  template:
    metadata:
      labels:
        app: ai-monitor
    spec:
      containers:
      - image: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ai-monitor:latest
        name: ai-monitor
        ports:
        - containerPort: 8085
        volumeMounts:
          - name: storage
            mountPath: /app/datasets
      volumes:
        - name: storage
          hostPath:
            path: /data/grafana_monitoring_service/datasets
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: ai-monitor-clusterip
  name: ai-monitor-clusterip
spec:
  ports:
  - port: 8085
    protocol: TCP
    targetPort: 8085
  selector:
    app: ai-monitor
  type: ClusterIP
```

## 5. Kubernetesリソースの生成
先ほど作成したマニフェストファイルからKubernetesリソースを生成します。
```sh
# ConfigMapの生成
kubectl apply -f manifests/cm_grafana-dashboards.yaml \
  -f manifests/cm_grafana-datasource.yaml \
  -f manifests/cm_prometheus.yaml

# prometheus/grafanaのリソース生成
kubectl apply -f manifests/depsvc_prometheus-grafana.yaml
# ai-monitorのリソース生成
kubectl apply -f manifests/depsvc_ai-monitor.yaml

# リソース生成の確認
# podのステータスが`Running`になることを確認します。初回起動時でも概ね1分以内でRunningになります。
# ※2秒間隔で表示が繰り返されます。podの起動が確認できたらctrl+cで終了してください
watch 'kubectl get pod,deploy,svc,cm'
```

## 6. データセットの生成およびMinikubeへの転送
データセットを生成しMinikubeへ転送します。この転送したデータはEvidently AIでのモニタリングでメトリクス算出する際のreferenceデータとして使用します。
```sh
# Evidently AIサンプルのフォルダに移動
cd evidently/examples/integrations/grafana_monitoring_service

# Pythonパッケージの追加
pip install -r requirements.txt

# データセットの生成
# ※サンプルにはkdd_k_neighbors_classifierというデータセットもあるのですが、
#   記事の執筆時点ではエラーが発生する現象が見られたので除外しています。
python scripts/prepare_datasets.py -d "bike_random_forest" -p "datasets/bike_random_forest"
python scripts/prepare_datasets.py -d "bike_gradient_boosting" -p "datasets/bike_gradient_boosting"

# minikube配下のローカルディレクトリへコピー
sudo cp -r datasets/bike_random_forest /var/lib/docker/volumes/minikube/_data/data/grafana_monitoring_service/datasets/
sudo cp -r datasets/bike_gradient_boosting /var/lib/docker/volumes/minikube/_data/data/grafana_monitoring_service/datasets/

# Grafanaダッシュボード定義ファイルのコピー
sudo cp -r dashboards/*.json /var/lib/docker/volumes/minikube/_data/data/grafana_monitoring_service/grafana-dashboards/

# ホームディレクトリに戻る
cd ~
```

## 7. ポートフォワードの実行
Minikubeの外部からアクセスできるようにポートフォワードを行います。
```sh
# prometheus、Grafanaのポートフォワード（バックグラウンド実行）
kubectl port-forward svc/prometheus-grafana-clusterip --address 0.0.0.0 9090 3000 >/dev/null &
# ai-monitorのポートフォワード（バックグラウンド実行）
kubectl port-forward svc/ai-monitor-clusterip --address 0.0.0.0 8085 >/dev/null &
```

## 8. データの送信とダッシュボードの確認
クライアントからデータを送信し、その結果をGrafanaのダッシュボードで確認します。
```sh
# Evidently AIサンプルのフォルダに移動
cd evidently/examples/integrations/grafana_monitoring_service

# データ送信のClient App実行
# これを実行すると2秒間隔で連続的にデータ送信する処理が約20分間動きます。
python scripts/example_run_request.py
```

データの送信が開始されたら、Grafanaのダッシュボード(http://localhost:3000)にアクセスします。
- 初回起動時のユーザIDと初期パスワードはadmin/adminです。初回ログイン後にパスワードの変更を求められます。

![sub003-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/4363ac53-fa87-7eb3-46eb-b501cfef875c.png)

メニューのDashboards > Browse でダッシュボードを選択します。例えば`Evidently Data Drift Dashboard`を選択すると以下のような画面が表示されます。

![sub003-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/52882767-873f-cd91-4d5e-fcdb73208df6.png)
![sub003-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/24ea9828-2e7f-17d2-aec2-d92bff43316d.png)
このダッシュボードでは、データドリフトの発生有無を時系列で表示しています。簡単に内部の動きを言うと、Client Appから送られてきたproductionデータとreferenceデータで統計的検定を行って両者のデータ分布に違いがあるかどうかで判定しています。検定は各特徴量ごとに行っており、一定数以上の特徴量において分布に違いがあった場合にデータドリフトが発生していると判断しています。  
今回のサンプルはproductionデータとreferenceデータが時期の異なるデータを使っているので、ドリフトが発生していると判断されやすいものになっています。


## 終了・削除手順
今回作成したリソースは下記のように終了・削除します。
```bash
# ポートフォワードの終了
# job番号は実際の数値に変更してください
jobs
kill %1
kill %2

# kubernetesリソースの削除
cd ~
kubectl delete -f manifests/depsvc_ai-monitor.yaml \
  -f manifests/depsvc_prometheus-grafana.yaml \
  -f manifests/cm_grafana-dashboards.yaml \
  -f manifests/cm_grafana-datasource.yaml \
  -f manifests/cm_prometheus.yaml

# ECRリポジトリの削除
aws ecr delete-repository --repository-name ai-monitor --force

# minikubeの停止
minikube stop
# ※minikubeを削除したい場合は`minikube delete`
```

## おわりに
Kubernetesの勉強をしながらのチャレンジだったのでかなり四苦八苦しましたが、おかげでKubernetesとEvidently AIの理解が深まりました。