AWSLab

DifyのバックアップをAMIで取る【S3の暗号鍵を忘れると復元できない】

スポンサーラベル
Difyのデータの所在。postgres・weaviate・redisはEC2のEBS上にありAMIで丸ごと取れるが、S3バケットにあるアップロードファイルと.dify_secret_key、privkeysの秘密鍵はAMIには入らない AWS

当サイトはアフィリエイト広告を利用しています。

Difyを業務で使い始めると、ナレッジやワークフロー、発行したAPIキーがどんどん溜まっていきます。CloudFormationでAWS上にDify環境を自動構築するで作ったようなEC2 1台構成だと、それらは全部そのインスタンスのEBS上にしか存在しません

バックアップとしてまず思いつくのはAMIです。実際それで大部分は守れます。ただ、Difyの場合はAMIを取っただけでは復元できません。 暗号鍵がEBSではなくS3側に置かれるためです。

本記事では、実際に東京リージョンの検証環境でバックアップを取りながら確認した手順をまとめます。

1. Difyのデータはどこにあるか

まず、何がどこに保存されているかを整理します。ここを把握しないと、取るべきバックアップの範囲が決まりません。

Difyのデータの所在。postgres・weaviate・redisはEC2のEBS上にありAMIで丸ごと取れるが、S3バケットにあるアップロードファイルと.dify_secret_key、privkeysの秘密鍵はAMIには入らない
データ中身保存先
PostgreSQLアカウント・ワークフロー・APIキーEC2のEBS
Weaviateナレッジのベクタ(埋め込み)EC2のEBS
Redisキュー・キャッシュEC2のEBS
アップロードファイルナレッジの元文書などS3
.dify_secret_keyDB内の資格情報を暗号化する鍵S3
privkeys/Difyが生成する秘密鍵S3

ファイルストレージをS3に切り替える手順はDifyのファイルストレージをS3にするで解説しました。このとき、Difyは起動時に.dify_secret_keyをバケットへ自動で書き込みます。疎通確認に使える便利な挙動なのですが、バックアップの観点では見落としの原因になります。

この鍵はDBに保存された資格情報(各モデルプロバイダーのAPIキーなど)を復号するために使われます。 AMIからインスタンスを戻しても、鍵が無ければDBの中身は読めても復号できません。

さらに厄介なのが、検証用テンプレートではバケットにDeletionPolicy: Deleteを指定している点です。スタックを削除すると、鍵もアップロードファイルも一緒に消えます。

2. AMIを取る

まずはEBS側です。インスタンスIDを指定してAMIを作成します。

# --no-reboot でインスタンスを止めずにイメージを取る
aws ec2 create-image \
  --instance-id i-xxxxxxxxxxxxxxxxx \
  --name "dify-blog-verify-$(date +%Y%m%d-%H%M)" \
  --description "Dify backup (no-reboot)" \
  --no-reboot \
  --tag-specifications \
    'ResourceType=image,Tags=[{Key=Name,Value=dify-backup}]' \
    'ResourceType=snapshot,Tags=[{Key=Name,Value=dify-backup}]' \
  --region ap-northeast-1

--tag-specificationsでAMIとスナップショットの両方にタグを付けておきます。片方だけだとスナップショットが無名で残り、後から棚卸しするときに何のバックアップか分からなくなります。

作成はすぐ返りますが、状態はpendingです。30GBのボリュームで10分前後かかります。

aws ec2 wait image-available --image-ids ami-xxxxxxxxxxxxxxxxx --region ap-northeast-1

進捗はスナップショット側で確認できます。

aws ec2 describe-snapshots --owner-ids self --region ap-northeast-1 \
  --query "Snapshots[].{Id:SnapshotId,State:State,Progress:Progress}" --output table

3. –no-reboot は安全なのか

--no-rebootはインスタンスを止めずにイメージを取るオプションです。停止したくない環境では必須ですが、ファイルシステムの整合性はAWSが保証しません。 ドキュメント上も、この場合の整合性は保証できないと明記されています。

ではDifyで使って問題ないのか。判断の材料は次のとおりです。

  • 取得されるのはボリューム全体の同一時点のイメージです。電源を引き抜いた状態と同じ扱いになります
  • PostgreSQLはWAL(先行書き込みログ)を持っており、この種のクラッシュから復旧できるように設計されています。起動時に自動でリカバリが走ります
  • Weaviateも起動時にコミット済みのデータから復旧しますが、PostgreSQLほど強い保証はありません

つまり--no-rebootは「多くの場合は復旧できるが、保証はない」という位置づけです。日次の自動バックアップならこれで十分でしょう。

3.1 厳密に整合性を取る場合

バージョンアップ前など、確実に戻せる状態を作りたいときは、コンテナを止めてから取ります。

# Session Manager でログインしてから実行する
cd /opt/dify/docker
sudo docker compose stop

この状態でAMIを取得し、終わったら起動し直します。

sudo docker compose start

コンテナが止まっている間はDifyが使えませんが、PostgreSQLが正常にシャットダウンされた状態のイメージが取れます。 ログインはAWS System Managerのセッションマネージャ経由で、SSH鍵もポート開放も不要です。

4. S3側の暗号鍵を退避する

ここが本題です。AMIには入らない.dify_secret_keyを別の場所に確保します。

バケットにコピーを置いても意味がありません。スタックを消せばバケットごと消えるからです。 バケットの外に出す必要があります。

4.1 SSMパラメータストアのSecureStringを使う

秘密情報の保管先としてまず候補になるのはAWS Secrets Managerですが、1シークレットあたり月0.40USDかかります。

一方、SSMパラメータストアのStandardティアは無料です。SecureStringを指定すればKMSで暗号化され、値の取得にも権限が要ります。今回のように数KBの鍵を数本置くだけなら、こちらで十分ですし追加コストがゼロで済みます

保管先費用上限
SSMパラメータストア(Standard)無料1件4KB
SSMパラメータストア(Advanced)約0.05USD/件/月1件8KB
Secrets Manager約0.40USD/件/月1件64KB

.dify_secret_keyは65バイト、秘密鍵は1,678バイトでした。Standardの4KBに収まります。

4.2 値を画面に出さずに退避する

ここで注意したいのが、鍵の中身をターミナルに表示させないことです。シェルの履歴やスクロールバッファに残ると、そこから漏れます。

S3から読んでそのままパラメータストアへ渡す、という処理をスクリプトにします。

import boto3, hashlib

s3 = boto3.client("s3", region_name="ap-northeast-1")
ssm = boto3.client("ssm", region_name="ap-northeast-1")
BUCKET = "<バケット名>"

items = [
    (".dify_secret_key", "/dify/dify-blog-verify/dify_secret_key",
     "DB内の資格情報の暗号化に使う。失うと復号不能"),
    ("privkeys/xxxxxxxx/private.pem", "/dify/dify-blog-verify/privkey",
     "Difyが生成した秘密鍵"),
]

for key, name, desc in items:
    body = s3.get_object(Bucket=BUCKET, Key=key)["Body"].read()
    ssm.put_parameter(
        Name=name, Value=body.decode("utf-8"), Type="SecureString",
        Description=desc, Overwrite=True, Tier="Standard",
    )
    # 値は出さない。長さとハッシュの一部だけを確認に使う
    print(f"{name}  {len(body)} bytes  sha256={hashlib.sha256(body).hexdigest()[:16]}")

Overwrite=Trueを付けているのは、鍵が再生成されたときに同じ名前で更新できるようにするためです。パラメータストアはバージョンを保持するので、上書きしても過去の値は残ります。

5. 退避できたか照合する

バックアップは「取れたつもり」が一番危険です。書き込んだ値がS3の原本と一致するかを、値を表示せずにハッシュで確認します。

import boto3, hashlib

ssm = boto3.client("ssm", region_name="ap-northeast-1")
s3 = boto3.client("s3", region_name="ap-northeast-1")

for name, key in pairs:
    got = ssm.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"].encode()
    src = s3.get_object(Bucket=BUCKET, Key=key)["Body"].read()
    ok = hashlib.sha256(got).hexdigest() == hashlib.sha256(src).hexdigest()
    print(f"{'一致' if ok else '不一致'}  {name}  ({len(got)} bytes)")

実際の出力です。

一致  /dify/dify-blog-verify/dify_secret_key  (65 bytes)
一致  /dify/dify-blog-verify/privkey  (1678 bytes)

WithDecryption=Trueを付けないと暗号化されたままの文字列が返るので、比較しても一致しません。

6. 自動で取り続ける

手動で取れることを確認したら、定期取得に切り替えます。Data Lifecycle Manager(DLM)を使うと、タグを条件にしてスケジュールを組めます。

  • 対象: Name=dify-blog-verify タグの付いたインスタンス
  • 頻度: 日次
  • 保持: 7世代

追加コストはスナップショットの保存容量分だけです。30GBのボリュームを7世代持っても、差分方式なので実際の課金対象は変更ブロック分に留まります。

世代数を決めずに取り続けると容量が増え続けるので、保持ポリシーは必ず設定します。

7. 復元の手順

戻すときの流れは次のようになります。

  1. AMIからインスタンスを起動する(同じセキュリティグループ・IAMロールを指定する)
  2. S3バケットが残っていれば、鍵はそのまま読まれるので何もしない
  3. バケットごと作り直した場合は、パラメータストアから鍵を書き戻す
# 鍵をバケットへ書き戻す(値をファイルに落とさずパイプで渡す)
aws ssm get-parameter --name /dify/dify-blog-verify/dify_secret_key \
  --with-decryption --query "Parameter.Value" --output text --region ap-northeast-1 \
  | aws s3 cp - s3://<新しいバケット名>/.dify_secret_key --region ap-northeast-1

Elastic IPを使っている場合は、新しいインスタンスに付け替えれば URL は変わりません。この点は別記事で扱っています。

なお、本記事では復元そのものは実施していません。 バックアップの取得と鍵の退避・照合までを実機で確認した内容です。復元手順は構成から導いたもので、実際に戻す前には検証環境で一度試すことをおすすめします。

8. まとめ

EC2にセルフホストしたDifyのバックアップで押さえる点を整理します。

  • DifyのデータはEBS側とS3側に分かれている。AMIだけでは片方しか守れない
  • .dify_secret_keyを失うと、AMIから戻してもDB内の資格情報を復号できない
  • 検証用テンプレートのバケットはDeletionPolicy: Deleteスタック削除で鍵ごと消える
  • 鍵の退避先はSSMパラメータストアのSecureStringが無料。Secrets Managerは月0.40USD
  • 鍵の中身はターミナルに出さない。長さとハッシュだけで確認する
  • --no-rebootはクラッシュ整合。PostgreSQLはWALで復旧できるが保証はない。

確実に戻したいときはdocker compose stopしてから取る

  • タグはAMIとスナップショットの両方に付ける。片方だと後で棚卸しできない

一番の落とし穴は、バックアップの範囲が構成に依存して変わることです。ファイルストレージをローカルのままにしていれば鍵もEBS上にあり、AMI 1本で済みます。S3へ外出しした結果として、守るべき対象が2か所に分かれました。

構成を変えたら、バックアップの範囲も見直す。 これはDifyに限らず言えることだと思います。

Dify環境そのものの構築についてはCloudFormationでAWS上にDify環境を自動構築する、ストレージのS3化についてはDifyのファイルストレージを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用語や構成に関しても習得できているはずです。

AWS認定ソリューションアーキテクト-アソシエイト問題集

あわせて読みたい

コメント

タイトルとURLをコピーしました