17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【簡単】Amazon S3を利用して、Difyでスケーラブルなストレージを実現する

Last updated at Posted at 2024-10-05

はじめに

お疲れ様です。矢儀 @yuki_ink です。

この記事では、Difyのストレージ領域としてAmazon S3を利用する方法についてご紹介します。

S3を利用したいと思った背景の話になりますが、先日EC2でDifyを構築しました。

EC2のストレージ領域はEBS。
RAG目的でサイズが大きなドキュメントをアップロードすると、ストレージの空き容量がゴリゴリ削られていきます。

EBSは拡張も面倒だし、お値段もそれなりだし、、
あ~~~ S3使いたい~~~!!!

ということで、以下の対応を行いました。

前提

以下の記事で構築した環境で作業を行います。
※Difyの初期構築の手順は以下の記事を参照してください。

EC2(RHEL9)での操作を想定しますが、ローカルPC / Windows など異なる環境でも、対応手順の大きな流れは変わりません。
DifyでS3を使いたいという方には、本記事は何かの参考になると思います。

やったこと

  1. S3バケットの作成
  2. IAMユーザ(アクセスキー)の作成
  3. Difyが稼働するサーバでの環境変数の設定

1. S3バケットの作成

Dify用にS3バケットを用意します。
今回はすべてデフォルト値で作成しました。
自環境のセキュリティ要件などに従って設定してください。
image.png

EC2-S3間の通信をVPCエンドポイント経由にしたい!という方は、VPCエンドポイントを作っておきましょう。
※Gatewayタイプの場合、ルートテーブルの設定もお忘れなく。
image.png

2. IAMユーザ(アクセスキー)の作成

DifyからS3バケットにアクセスするためのIAMユーザを作成し、アクセスキーとシークレットアクセスキーを取得します。
アクセスキーの作成方法は以下の記事を参考にしてください。

IAMポリシーについては、今回は AmazonS3FullAccess をアタッチしました。
自環境のセキュリティ要件などに従って設定してください。
また、S3を作成する際にKMSの暗号化を有効化した場合、KMSキーの利用に必要な権限もあわせて付与します。
image.png

当初、IAMユーザ(アクセスキー)ではなくIAMロールで制御したいと思っていたんですが、現状のDifyの標準機能ではIAMロールは対応しておらず、アクセスキーの利用が必要となるようです。

# IAMロールでやろうとしたときに出たエラー
api-1 | botocore.exceptions.ClientError: An error occurred (AuthorizationHeaderMalformed) when calling the PutObject operation: The authorization header is malformed; a non-empty Access Key (AKID) mustbe provided in the credential.

3. Difyが稼働するサーバでの環境変数の設定

Difyが稼働するサーバに接続し、以下の環境変数を設定します。

環境変数名
STORAGE_TYPE s3
STORAGE_LOCAL_PATH storage 
S3_ENDPOINT https://s3.(リージョン名).amazonaws.com
(例)https://s3.us-west-2.amazonaws.com 
S3_BUCKET_NAME 1. S3バケットの作成 で作成したバケットの名前
(例)dify-s3-bucket
S3_ACCESS_KEY 2. IAMユーザ(アクセスキー)の作成 で取得したアクセスキーの値
S3_SECRET_KEY 2. IAMユーザ(アクセスキー)の作成 で取得したシークレットアクセスキーの値
S3_REGION ※S3バケットを作成したリージョン
(例)us-west-2

各環境変数についての詳細は公式ドキュメント「環境変数の説明」をご確認ください。

Linux環境では、以下のコマンドで設定できます。
ついでに、/etc/profile にも書いておきましょう。

export STORAGE_TYPE='s3'
export STORAGE_LOCAL_PATH='storage'
export S3_ENDPOINT='https://s3.us-west-2.amazonaws.com'
export S3_BUCKET_NAME='dify-s3-bucket'
export S3_ACCESS_KEY='xxxxxxxxxxxxx'
export S3_SECRET_KEY='yyyyyyyyyyyyyyyyyyyyyyyyyyyy'
export S3_REGION='us-west-2'

ここまで来たら、サービスを再起動します。

cd dify/docker

#-d オプションをつけて、バックグラウンドで起動
docker compose up -d

dify/docker/docker-compose.yaml の中身を見てみると、以下のような記述があり、ホストの環境変数を利用しているのが分かります。

  S3_ENDPOINT: ${S3_ENDPOINT:-}
  S3_BUCKET_NAME: ${S3_BUCKET_NAME:-}
  S3_ACCESS_KEY: ${S3_ACCESS_KEY:-}
  S3_SECRET_KEY: ${S3_SECRET_KEY:-}
  S3_REGION: ${S3_REGION:-us-east-1}

動作確認

Difyにログインし、ナレッジの作成 からドキュメントをアップロードします。
今回はQiita CLIの README を利用します。
image.png

問題なくナレッジを作成することができました。
image.png

簡単なRAGワークフローでテストしてみます。
image.png
大丈夫そう!!

S3バケットを確認すると、upload_files/ というフォルダが作成されており、配下にそれらしきファイルが格納されていました。
image.png

終わりに

以上、DifyとS3を連携させ、スケーラブルかつ安価なストレージを利用できるようになりました。
これでDifyに大きめのドキュメントをアップロードするときの罪悪感が軽くなりますw

Difyに関しては、AWS公式からもブログが出るほどAWSとの相性もよく、yamlファイルをいじらなくてもある程度のことはできそうです。
今後もDifyとAWSサービスの連携について、色々検証できたらと思います。

最後までお目通しいただき、ありがとうございました。

Tips (2024/12/3追記)

ある日突然、こんな事象が発生しました。

  • Difyを開き、作成済みのチャットボットでプロンプトを入力すると、「Internal Server Error」というエラーメッセージが表示される。
    ※以前は使えていたものでもNG
  • ワークフロー選択画面で何か編集しようとしても、同様に「Internal Server Error」というエラーメッセージが表示される。
  • Difyの設定画面でモデル一覧が読み込めない。

docker logs docker-api-1 コマンドでログを確認すると、こんなエラーが発生していました。

2024-12-03 08:38:18,900.900 ERROR [Dummy-12] [app.py:838] - Exception on /console/api/workspaces/current/model-providers [GET]
Traceback (most recent call last):
  File "/app/api/extensions/storage/s3_storage.py", line 48, in load_once
   data = client.get_object(Bucket=self.bucket_name, Key=filename)["Body"].read()
  File "/app/api/.venv/lib/python3.10/site-packages/botocore/client.py", line 565, in _api_call
   return self._make_api_call(operation_name, kwargs)
  File "/app/api/.venv/lib/python3.10/site-packages/botocore/client.py", line 1017, in _make_api_call
   raise error_class(parsed_response, operation_name)
botocore.errorfactory.NoSuchKey: An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/api/libs/rsa.py", line 55, in get_decrypt_decoding
   private_key = storage.load(filepath)
  File "/app/api/extensions/ext_storage.py", line 43, in load
   return self.load_once(filename)
  File "/app/api/extensions/ext_storage.py", line 46, in load_once
   return self.storage_runner.load_once(filename)
  File "/app/api/extensions/storage/s3_storage.py", line 51, in load_once
   raise FileNotFoundError("File not found")
FileNotFoundError: File not found

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/api/.venv/lib/python3.10/site-packages/flask/app.py", line 880, in full_dispatch_request
   rv = self.dispatch_request()
  File "/app/api/.venv/lib/python3.10/site-packages/flask/app.py", line 865, in dispatch_request
   return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
  File "/app/api/.venv/lib/python3.10/site-packages/flask_restful/__init__.py", line 489, in wrapper
   resp = resource(*args, **kwargs)
  File "/app/api/.venv/lib/python3.10/site-packages/flask/views.py", line 110, in view
   return current_app.ensure_sync(self.dispatch_request)(**kwargs)  # type: ignore[no-any-return]
  File "/app/api/.venv/lib/python3.10/site-packages/flask_restful/__init__.py", line 604, in dispatch_request
   resp = meth(*args, **kwargs)
  File "/app/api/controllers/console/setup.py", line 65, in decorated
   return view(*args, **kwargs)
  File "/app/api/libs/login.py", line 93, in decorated_view
   return current_app.ensure_sync(func)(*args, **kwargs)
  File "/app/api/controllers/console/wraps.py", line 22, in decorated
   return view(*args, **kwargs)
  File "/app/api/controllers/console/workspace/model_providers.py", line 38, in get
   provider_list = model_provider_service.get_provider_list(tenant_id=tenant_id, model_type=args.get("model_type"))
  File "/app/api/services/model_provider_service.py", line 46, in get_provider_list
   provider_configurations = self.provider_manager.get_configurations(tenant_id)
  File "/app/api/core/provider_manager.py", line 134, in get_configurations
   custom_configuration = self._to_custom_configuration(
  File "/app/api/core/provider_manager.py", line 640, in _to_custom_configuration
   self.decoding_rsa_key, self.decoding_cipher_rsa = encrypter.get_decrypt_decoding(tenant_id)
  File "/app/api/core/helper/encrypter.py", line 34, in get_decrypt_decoding
   return rsa.get_decrypt_decoding(tenant_id)
  File "/app/api/libs/rsa.py", line 57, in get_decrypt_decoding
   raise PrivkeyNotFoundError("Private key not found, tenant_id: {tenant_id}".format(tenant_id=tenant_id))
libs.rsa.PrivkeyNotFoundError: Private key not found, tenant_id: 5bc686ba-491a-433c-bf79-7be863327732

2. ローカル展開ログで「ファイルが見つかりません」というエラーを修正する方法は? を参考にして、 docker exec -it docker-api-1 flask reset-encrypt-key-pair コマンドを実行。
image.png

一応復旧しました、、
心当たりのある作業がないのですが、いったい何だったのか、、

もしかしたらこっちだったかも(未検証)

image.png

17
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?