Difyは既定でアップロードしたファイルをapiコンテナ内のstorage/に保存します。コンテナを作り直すとファイルごと消えるため、検証以外の用途ではS3などに逃がすのが定石です。
.env側の設定についてはDifyの.envを理解するで解説しました。本記事では、そこで扱えなかったAWS側の準備——CloudFormationでのバケットとIAMポリシーの定義、そして稼働中の環境への適用手順を、実際に東京リージョンで動かして確認した内容でまとめます。
Bedrockのときと同じく、アクセスキーを一切発行せずインスタンスロールのままS3を使えます。
1. 必要なのは2つだけ
用意するのはS3バケットと、EC2のインスタンスロールに付けるポリシーだけです。Dify側は.envの書き換えで済みます。
ここでひとつ、IAMポリシーの書き方に注意点があります。

バケットに対する操作とオブジェクトに対する操作では、指定するARNが違います。 s3:ListBucketはバケット自体(arn:aws:s3:::バケット名)を、s3:GetObjectはその中のオブジェクト(arn:aws:s3:::バケット名/*)を対象にします。
末尾の/*の有無で対象が変わるため、1つのステートメントにまとめると、ファイルの読み書きか一覧取得のどちらかがアクセス拒否になります。地味ですが引っかかりやすいところです。
2. CloudFormationでバケットとポリシーを定義する
既存のテンプレートに追加するのは、バケットのリソースとロールのポリシー2つです。
Resources:
# Dify のファイルストレージ用バケット
StorageBucket:
Type: AWS::S3::Bucket
# 検証環境なのでスタック削除時に一緒に消す。
# 中身が残っていると削除に失敗するので、先にバケットを空にすること。
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
# Dify は S3_USE_AWS_MANAGED_IAM=true にすると
# インスタンスロールの権限で S3 にアクセスする
- PolicyName: S3Storage
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ListOwnBucket
Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
# バケット自体への操作
Resource: !GetAtt StorageBucket.Arn
- Sid: ObjectAccess
Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
# オブジェクトへの操作。この2つを分けないと通らない
Resource: !Sub ${StorageBucket.Arn}/*
Outputs:
StorageBucketName:
Value: !Ref StorageBucket
EmptyBucketCommand:
Description: スタック削除前にバケットを空にするコマンド
Value: !Sub aws s3 rm s3://${StorageBucket} --recursive
DeletionPolicy: Deleteにしていますが、中身が残っているバケットは削除できません。スタックを消す前にバケットを空にしてください。Outputsにそのコマンドを出しておくと、後片付けのときに探さずに済みます。
3. 稼働中の環境に適用するときの注意
すでに動いているDify環境にこの変更を当てる場合、知っておくべき挙動が2つあります。
3.1 UserDataは再実行されない
テンプレートのUserDataに.envの書き換えを仕込んでも、UserDataはインスタンスの初回起動時にしか実行されません。既存インスタンスにスタック更新をかけても、.envは書き換わらないままです。
新規構築なら自動化できますが、稼働中の環境には手で反映する必要があります(手順は次章)。
3.2 UserDataを変えるとEC2が再起動する
変更セットを作ると、EC2がReplacement: Conditionalと表示されます。ここで身構えるところですが、AWS公式ドキュメントには次のように書かれています。
If the root volume is an EBS volume and you update user data, CloudFormation restarts the instance. If the root volume is an instance store volume and you update user data, the instance is replaced.
つまりルートボリュームがEBSなら再起動のみで、インスタンスは置き換わりません。実際に適用したところ、インスタンスIDは変わらずデータも残りました。
ただしパブリックIPは変わります。Elastic IPを割り当てていない場合、アクセス先のURLが変わる点に注意してください。
適用前には必ず変更セットで影響を確認します。
# 変更内容だけ確認する(何も変更しない)
aws cloudformation create-change-set \
--stack-name dify \
--change-set-name plan \
--template-body file://dify-cloudformation.yaml \
--capabilities CAPABILITY_IAM \
--parameters ParameterKey=VpcId,UsePreviousValue=true
aws cloudformation describe-change-set \
--stack-name dify --change-set-name plan \
--query "Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Replacement:Replacement}" \
--output table
4. 権限が効いているか先に確かめる
Difyの設定を変える前に、インスタンスロールでS3が操作できるかを確認しておきます。ここで通らなければ、あとの切り分けが面倒になります。
# Session Manager でログインして実行する
BUCKET=<Outputsに出力されたバケット名>
# 一覧・書き込み・読み出し・削除を一通り試す
aws s3 ls s3://$BUCKET/
echo hello > /tmp/t.txt && aws s3 cp /tmp/t.txt s3://$BUCKET/test.txt
aws s3 cp s3://$BUCKET/test.txt -
aws s3 rm s3://$BUCKET/test.txt
アクセスキーを設定していないのに動けば、インスタンスロールが効いています。
5. Dify側の設定を反映する
.envを書き換えてコンテナを入れ直します。既存のSTORAGE_TYPE行を置き換えるのを忘れないでください(既定はopendalです)。
cd /opt/dify/docker
# 元の設定を控えておく
cp .env .env.bak
# 保存先を S3 に切り替える
sed -i "s/^STORAGE_TYPE=.*/STORAGE_TYPE=s3/" .env
cat >> .env <<EOF
# --- S3 storage (IAM role) ---
S3_USE_AWS_MANAGED_IAM=true
S3_BUCKET_NAME=<バケット名>
S3_REGION=ap-northeast-1
S3_ADDRESS_STYLE=auto
PLUGIN_S3_USE_AWS=true
PLUGIN_S3_USE_AWS_MANAGED_IAM=true
EOF
# 環境変数を読み直させる
docker compose down
docker compose up -d
docker compose downはコンテナを削除しますが、名前付きボリュームは残るのでデータベースの中身は消えません。
反映されたかはコンテナの環境変数で確認できます。
docker compose exec -T api env | grep -E "^(STORAGE_TYPE|S3_BUCKET_NAME|S3_USE_AWS_MANAGED_IAM)"
6. 【重要】稼働中の環境では既存ファイルの移行が必要
ここが今回いちばんハマったところです。新規構築なら起きませんが、すでに動いている環境を切り替える場合は必ず必要な作業です。
6.1 症状
.envを書き換えてコンテナを入れ直したあと、Difyの画面を開くと「連携」→「モデルプロバイダー」やナレッジの画面が軒並みInternal Server Errorになりました。ログインはでき、トップページも表示されるため、一見すると原因がわかりません。
apiコンテナのログを見ると、こう出ていました。
botocore.errorfactory.NoSuchKey: An error occurred (NoSuchKey) when calling
the GetObject operation: The specified key does not exist.
...
libs.rsa.PrivkeyNotFoundError: Private key not found, tenant_id: xxxxxxxx-xxxx-...
6.2 原因
Difyは、モデルプロバイダーに登録した認証情報を暗号化して保存しています。その暗号化に使うRSA秘密鍵を、ファイルストレージ層に置いているのです。
privkeys/<tenant_id>/private.pem
STORAGE_TYPEを切り替えると、この鍵の参照先もローカルディスクからS3に変わります。ところが鍵の実体はローカルに残ったままなので、S3を探しても見つかりません。認証情報を復号できず、プロバイダーを扱う処理がすべて落ちるわけです。
エラーメッセージがPrivkeyNotFoundErrorなので、ストレージ設定が原因だと結びつけにくいのが厄介です。
6.3 対処
切り替え前のローカルディレクトリからS3へファイルを移します。
BUCKET=<バケット名>
STORAGE=/opt/dify/docker/volumes/app/storage
# 何が残っているか確認する
find $STORAGE -type f
/opt/dify/docker/volumes/app/storage/.dify_secret_key
/opt/dify/docker/volumes/app/storage/privkeys/xxxxxxxx-xxxx-.../private.pem
/opt/dify/docker/volumes/app/storage/.init_permissions
秘密鍵と.dify_secret_keyをS3へコピーします。
# 暗号化用の秘密鍵(これが無いと認証情報を復号できない)
aws s3 sync $STORAGE/privkeys s3://$BUCKET/privkeys
# S3側は起動時に新しく生成されているので、元のファイルで上書きする
aws s3 cp $STORAGE/.dify_secret_key s3://$BUCKET/.dify_secret_key
.dify_secret_keyの上書きを忘れないでください。S3に切り替えた状態でDifyを起動すると、鍵が無いと判断して新しいものを生成します。それをそのままにすると、データベース側に保存済みの暗号化データと食い違ったままになります。
移した後、関連コンテナを再起動します。
cd /opt/dify/docker
docker compose restart api worker plugin_daemon
アップロード済みのファイルやナレッジの原本がある環境では、$STORAGE配下をまるごと同期してください。
aws s3 sync $STORAGE s3://$BUCKET --exclude ".init_permissions"
6.4 復旧の確認
モデルプロバイダーの画面が開けるようになれば復旧しています。ブラウザの開発者ツールから確認する方法もあります。
// 200 が返り、Bedrock が active になっていれば復旧
await fetch('/console/api/workspaces/current/model-providers', {credentials:'include'})
.then(r => r.status)
7. 動作確認
起動後にバケットを覗くと、Dify自身が.dify_secret_keyを書き込んでいます。
aws s3 ls s3://$BUCKET/ --recursive
2026-08-16 14:00:13 65 .dify_secret_key
これが出ていれば、Difyのストレージ層がS3を向いています。ファイルをアップロードする前に疎通確認ができるので、覚えておくと便利です。
8. まとめ
DifyのファイルストレージをS3にする際の要点を整理します。
- IAMポリシーはバケットとオブジェクトでResourceを分ける。
/*の有無で対象が変わる S3_USE_AWS_MANAGED_IAM=trueで、アクセスキーを発行せずインスタンスロールのまま使える- UserDataは初回起動時にしか実行されない。稼働中の環境には
.envを手で反映する - UserDataを変えるとEC2は再起動する。EBSルートなら置き換えではないが、パブリックIPは変わる
- 稼働中の環境を切り替えるなら、既存の
storage/をS3へ移すこと。
秘密鍵(privkeys/)を移し忘れるとPrivkeyNotFoundErrorで画面が500になる
- 起動時に
.dify_secret_keyがバケットに書かれるので、それで疎通を確認できる - スタック削除前にバケットを空にする
とくに5つ目は、新規構築では起きないぶん見落としやすい落とし穴です。既存環境の保存先を後から変えるときは、設定を変える前にストレージの中身を確認する癖をつけておくと安全です。
本記事の内容は東京リージョンで実際に構築して確認したものです。Dify 1.16.1での動作になります。
Dify環境そのものの構築についてはCloudFormationでAWS上にDify環境を自動構築する、モデルプロバイダーの設定についてはDifyにAWS Bedrockを設定するで解説しています。BedrockもS3も同じインスタンスロールで賄えるので、この構成なら長期間有効な認証情報をサーバー上に一切置かずに済みます。
AWSを効率的に学習する方法
私が効率的にAWSを学習するために実施した方法は以下の通りです。
①最初に書籍(ハンズオンができる)を購入、座学でAWSの基礎を学習
②AWS資格試験を取得ための学習
※私の場合は①と②を合わせて2か月でソリューションアーキテクトを取得できました。
①AWS基礎学習
最初に購入した書籍は「Amazon Web Services 基礎からのネットワーク&サーバー構築」です。
Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版

この本では、AWSの基本サービスを利用したハンズオンを通じて、AWSの基礎を学習することができます。
また、タイトル通りAWSのネットワークやインフラに関しても網羅しているため、もともとインフラ系の技術者ではない人たちにとっても分かりやすい内容だと思います。
とりあえずAWS上にサーバーを設定して開発を行うための準備までするには最良の一冊です。
Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版
②AWS資格取得
AWSの基礎をある程度学習することができたら、次はAWS資格を取得しましょう。まずはAWSソリューションアーキテクトを目指しましょう。

資格勉強のための問題集をひたすら解きながら、AWSの知識を積み重ねて習得していきましょう!
苦行ではありますが、この問題集を3周ほどすればAWS用語や構成に関しても習得できているはずです。
独学だと手が止まってしまう場合は、ササエル のようなインフラエンジニア向けのオンラインスクールを使う手もあります。ネットワークやサーバーの分野に絞ったカリキュラムなので、業務でインフラを触る人が学び直すのにも使えます。


コメント