Nutanix Kubernetes Platform (NKP) + Nutanix Enterprise AI (NAI) + DifyでオンプレおもしろRAG環境を構築する ②Dify編 

# はじめに
[Nutanix Advent Calender 2024](https://adventar.org/calendars/10213)のための記事のパート２です。クリスマス過ぎてしまった。
[前回](https://qiita.com/jyoshise/items/559df72040468c600667)NKPでKubernetesクラスタをデプロイしたので、そこにDifyをデプロイしていきます。

# DifyをKubernetes上で（そこそこちゃんとした構成で）動かす
Difyはクラウドサービス（SaaS)として提供されているものと、ローカルインストールできるオープンソースのコミュニティ版があります。

https://dify.ai/jp

[ドキュメント](https://docs.dify.ai/ja-jp/getting-started/install-self-hosted/environments)ではDocker Composeによるコミュニティ版のデプロイ方法が説明されていますが、これはお手軽にパソコンで動かしてみる程度のものであり、ちゃんと使おうと思うとバックエンドのサービスも含めてしっかり構成する必要があります。
そこでKubernetesを使うわけですが、コミュニティ有志がhelmチャートを公開してくれています。ここではこちらを使います。

https://github.com/douban/charts/tree/master/charts/dify

Docker Composeでデプロイする場合は、Difyがバックエンドで使う
- RDB(postgresql)
- NoSQL(redis)
- オブジェクトストレージ（minio）
- ベクターストア（weaviate)

もコンテナとしてデプロイされます。が、それだけだとスケーラビリティや可用性は考慮されておらず、とりあえず動くという最小構成です。
上記のhelmチャートを利用してKubernetes上で展開すると、READMEに書かれているように、これらのバックエンドサービスを外部化することができます。
特にオンプレのデータを活用したRAGというユースケースを考慮すると、オブジェクトストレージとベクターストアにはスケーラビリティ・アベイラビリティ・セキュリティが求められますので、ここでは

- オブジェクトストレージ： Nutanix Objectsを利用
- ベクターストア： Kubernetes上にMilvusを展開

とします。もちろんRDBにNutanix Database Service(NDB)を使ってもよい（ていうか使ってほしい）のですが、そこはまあやるだけなので今回はパスします。

## Nutanix Objectsのセットアップ
やってみればわかるので細かく説明はしませんが、以下の流れです。
- `Create Object Store` でオブジェクトストレージのVMを作成
    こんな感じでウィザードぽちぽちしていけば作れます。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103746/0c59f446-e5fb-331f-a4a9-f584eabb1be5.png)
- `Create Bucket` でバケットを作成
    名前つけて作るだけです。Dify用（アップロードした生データなどを格納）の`dify`というバケットと、後述するMilvus（ベクターストア）用の`Milvus`というバケットを作りました。
- ユーザーを作ってアクセスキーを発行し、作成したバケットに紐づけ

## Milvusのデプロイ
オープンソースのベクターストア（ベクトルDBという呼び方の方が一般的かもしれませんが、個人的には「DB」という概念とはなんか違う感じがするのでVector Storeという呼び方が好みです）であるMilvusは、それ自体かなりマイクロサービス・分散アーキテクチャになっています。

https://milvus.io/

Milvusには公式のHelmチャートが用意されているので、そちらを使ってデプロイします。

https://milvus.io/docs/install_cluster-helm.md

前回`ai-demo`というKubernetesクラスタを作成したので、そこにデプロイします。以下、Kubeconfigの向き先はこの`ai-demo`クラスタです。

:::note
余談ですが、 複数のK8sクラスタに対して作業を行う際、Kubeconfigがどのクラスタを向いているか混乱しがちなので、私はkubectxを使っています。
https://github.com/ahmetb/kubectx
:::

まず、[Helmチャートのvalues.yaml](https://github.com/zilliztech/milvus-helm/blob/master/charts/milvus/values.yaml) をダウンロードして、以下を編集します。

デフォルトではオブジェクトストレージとしてminioが展開されますが、今回はNutanix Objectsを使うので不要です。
```
minio:
  enabled: false
```

Nutanix Objectsを利用するための設定をします。S3用の項目があるので、以下のように編集します。
```
###################################
# External S3
# - these configs are only used when `externalS3.enabled` is true
###################################
externalS3:
  enabled: true
  host: "<Object StorageのIPアドレス>"
  port: "80"
  accessKey: "<バケットに割り当てたユーザーのアクセスキー>"
  secretKey: "<バケットに割り当てたユーザーのシークレットキー>"
  useSSL: false
  bucketName: "milvus"
  rootPath: ""
  useIAM: false
  cloudProvider: "aws"
  iamEndpoint: ""
  ```

あとはこのvalues.yamlを指定してデプロイするだけです。
```
$ helm repo add milvus https://zilliztech.github.io/milvus-helm/
$ helm repo update
$ kubectl create ns milvus
$ helm install -n milvus my-release milvus/milvus -f values.yaml
```
問題がなければ5分ぐらいで上がってくると思います。
```
$ kubectl -n milvus get pod
NAME                                           READY   STATUS    RESTARTS   AGE
my-release-etcd-0                              1/1     Running   0          74s
my-release-etcd-1                              1/1     Running   0          2m47s
my-release-etcd-2                              1/1     Running   0          4m12s
my-release-milvus-datanode-86486d7d7-jmgpk     1/1     Running   0          4m14s
my-release-milvus-indexnode-d48945776-mljwj    1/1     Running   0          4m14s
my-release-milvus-mixcoord-69d65f8449-q48m8    1/1     Running   0          4m13s
my-release-milvus-proxy-5bd5584b4c-d9445       1/1     Running   0          4m14s
my-release-milvus-querynode-76878878bb-4wxhr   1/1     Running   0          4m14s
my-release-pulsarv3-bookie-0                   1/1     Running   0          18m
my-release-pulsarv3-bookie-1                   1/1     Running   0          18m
my-release-pulsarv3-bookie-2                   1/1     Running   0          18m
my-release-pulsarv3-broker-0                   1/1     Running   0          18m
my-release-pulsarv3-broker-1                   1/1     Running   0          18m
my-release-pulsarv3-proxy-0                    1/1     Running   0          18m
my-release-pulsarv3-proxy-1                    1/1     Running   0          18m
my-release-pulsarv3-recovery-0                 1/1     Running   0          18m
my-release-pulsarv3-zookeeper-0                1/1     Running   0          18m
my-release-pulsarv3-zookeeper-1                1/1     Running   0          18m
my-release-pulsarv3-zookeeper-2                1/1     Running   0          18m
```

## Ingressを作る
NKPにはTreafikが標準で入っていて、これをIngressコントローラーとして使うことができます。

https://github.com/traefik/traefik

が、NKP独特のKommanderというアプリケーション管理の仕組みがあり、TraefikのIngressはKommander管理下のアプリケーションで使用することが想定されています。NKPの管理画面（Dashboard）もそのひとつです。
私は個人的にそういうOpinionatedなのがちょっと気持ち悪いので、ここでは普通に `ingress-nginx` を入れて使うことにします。

https://github.com/kubernetes/ingress-nginx

コマンド一発
```
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace
```


## Difyをデプロイする
前述したようにHelmチャートを使用してデプロイしますので、`values.yaml`ファイルを作ります。
```yaml:values.yaml
global:
  # Ingressが解釈するホスト名。ingress-nginxのExternal IPでDNSに登録しておくか、クライアントのhostsに書いておく
  host: "aidemo.japansa.local"　
  enableTLS: false
  storagetype: "s3"

  image:
    # Set to the latest version of dify
    # Check the version here: https://github.com/langgenius/dify/releases
    # If not set, Using the default value in Chart.yaml
    tag: "0.6.15"
  extraBackendEnvs:
  - name: SECRET_KEY
    value: <なんか生成したやつ>
  - name: LOG_LEVEL
    value: "DEBUG"
  
  # ベクターストアとしてmilvusを使用
  # ClusterIPでアクセスするので、kubectl -n milvus get svcでmy-release-milvusのClusterIPを確認しておく
  # ユーザー・パスワードはデフォルトを書いてるけど良い子はまねしないでね
  - name: VECTOR_STORE
    value: "milvus"
  - name: MILVUS_HOST
    value: "10.100.58.48"
  - name: MILVUS_PORT
    value: "19530"
  - name: MILVUS_USER
    value: "root"
  - name: MILVUS_PASSWORD
    value: "Milvus"

  # Nutanix Objectsをオブジェクトストレージとして使用。バケットとキーはあらかじめ作っておく
  - name: S3_ENDPOINT
    value: "http://<Object StoreのIP>"
  - name: S3_BUCKET_NAME
    value: "dify"
  - name: S3_ACCESS_KEY
    value: "<バケットに割り当てたユーザーのアクセスキー>"
  - name: S3_SECRET_KEY
    value: "<バケットに割り当てたユーザーのシークレットキー>"
  
  # ユーザー登録時のメール認証のためのSMTPサーバー
  - name: MAIL_TYPE
    value: "smtp"
  - name: SMTP_SERVER
    value: "<SMTPサーバーのIP>"
  - name: SMTP_PORT
    value: "587"

# ingressのクラスを指定。NKPではデフォルトでkommander-traefikというクラスが使用できるが、ここでは別途nginx-ingressを導入して使用している
ingress:
  enabled: true
  className: "nginx"

# オブジェクトストレージはNutanix Objectsを使用するのでminioは無効化
minio:
  embedded: false

# DB（Postgresql）はNDBを使用してHA構成としてもよいが、ここでは組み込みPostgresを使用
postgresql:
  embedded: true
```

で、デプロイ
```
$ helm repo add douban https://douban.github.io/charts/
$ kubectl create ns dify
$ helm upgrade -n dify dify douban/dify -f values.yaml --install --debug
```

問題なければ2～3分で上がってくると思います。
```
$ kubectl -n dify get pod
NAME                             READY   STATUS    RESTARTS   AGE
dify-api-64df859d74-jt6t6        1/1     Running   0          2m6s
dify-frontend-775dc4c99c-q8z8v   1/1     Running   0          2m6s
dify-postgresql-0                1/1     Running   0          2m6s
dify-redis-master-0              1/1     Running   0          2m6s
dify-sandbox-5c6bdcdb9c-kj6lh    1/1     Running   0          2m6s
dify-worker-c96bf9cf8-qhvgt      1/1     Running   0          2m6s
```

DBをアップグレードしろって書いてあるのでやっておきます。
```
$ kubectl -n dify exec -it dify-api-64df859d74-jt6t6 -- flask db upgrade
$ kubectl -n dify exec -it dify-worker-c96bf9cf8-qhvgt -- flask db upgrade
```

で、登録したホスト名にブラウザからアクセスすると初期設定画面が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103746/715d7048-bd97-516a-4e1b-1c485db1e50b.png)

管理者ユーザーを登録してログインすると、DifyのUIが開きます。ぱちぱちぱち。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103746/1fa0f457-820a-669d-68fc-921a775da252.png)

つづく。