🚀

Cloud Run×VMでDifyを本番運用: 失敗から学ぶ5つの回避策

に公開

はじめに

こんにちは!チーム Manabi-DX-Crew です。

私たちは第3回 AI Agent Hackathon with Google Cloud(8月5日〜9月24日)に参加し、「Session Buddy」という AI エージェントシステムを開発しました。本記事では、特にインフラ構築に集中した 約3週間(9月5日〜24日) で、このシステムをゼロから本番運用レベルまで引き上げる過程で直面したリアルな失敗と、その回避策を共有します。

「Session Buddy」のシステム全体像は以下の記事にまとめています。
https://zenn.dev/manabi_dx_crew/articles/d13ab381645876

この記事で得られる知見 (TL;DR)

  • やったこと: Cloud Run と Compute Engine を組み合わせ、スケーラビリティと柔軟性を両立するハイブリッド構成を構築。
  • 直面した失敗: Preemptible VM の24時間停止や動的IPによるサービス断、誤ったコマンド選択によるバックアップ失敗。
  • 確立した回避策: 静的IPを持つ通常VMへの移行、gcloud コマンドの正確な利用、Cloud Storage ライフサイクルによる宣言的な世代管理。

記事の対象読者

  • Google Cloud でマイクロサービスを構築したい方
  • AI 系サービスのインフラ運用に興味がある方
  • Preemptible/Spot VM と通常 VM の使い分けに迷っている方
  • Cloud Run を本番運用に載せるための実践的な知見を探している人

システムアーキテクチャ

全体構成

Dify のように複数コンテナで構成されるアプリを、単一コンテナ実行に強い Cloud Run と連携させるため、私たちは以下のハイブリッド構成を採用しました。

:::message / :::details は Zenn の拡張記法です。詳しくは Zenn の Markdown 記法 を参照。

技術スタック

フロントエンド(Cloud Run)

  • Avatar UI (avatar-ui-core-service): Python 3.9 + Flask
  • UI: Vanilla JavaScript, CSS3

プロキシ(Cloud Run)

  • Proxy (dify-session-buddy): nginx (alpine)

バックエンド(Compute Engine)

  • VM: e2-standard-2(2 vCPU, 8GB RAM)
  • Dify: 1.8.1 (コンテナ内部では Python 3.10 を使用)

開発の軌跡:直面した課題と回避策

Phase 1: Preemptible VM の罠と通常 VM への移行

課題:安価だが不安定な Preemptible VM

ハッカソン初期、コスト最優先で Preemptible VM を選択したところ、24時間での強制停止動的IP により、デモ直前にサービスが停止するリスクに直面しました。

Preemptible VM と Spot VM の違い(要点)
  • Preemptible VM(旧世代): --preemptible最大24時間で必ず停止します。
  • Spot VM(現行): --provisioning-model=SPOT24時間上限なし。リソース逼迫時にいつでも停止されうるVMです。

回避策:静的IPを持つ通常VMへ切り替え

安定運用の前提として、常時稼働固定の外部IPを確保する必要がありました。

静的IPアドレスの予約と正しい割り当て

--address フラグの挙動は、VMを新規作成するか、既存VMに付与するかで異なるため、特に注意が必要なポイントでした。

# 1) 静的IPアドレスを予約
gcloud compute addresses create dify-static-ip --region=asia-northeast1

# 2) 予約した「IPアドレス文字列」を取得
STATIC_IP=$(gcloud compute addresses describe dify-static-ip \
  --region=asia-northeast1 --format='get(address)')

# 3) "新規" VM 作成時にその IP を割り当てる
gcloud compute instances create dify-production \
  --zone=asia-northeast1-c \
  --machine-type=e2-standard-2 \
  --address="${STATIC_IP}"

Phase 2: 柔軟なプロキシ設定とデプロイ自動化

課題:IP のハードコードと曖昧な認可設定

Nginx に VM の IP をハードコードしていたため保守性が低く、さらに Cloud Run の公開/非公開の方針が曖昧なままだと 403 エラーなど認可事故の温床になります。

回避策:動的設定と「公開」の明示

  • デプロイ時に VM の外部 IP を自動取得し、環境変数で注入する。
  • 公開するサービスは --allow-unauthenticated明示的に公開 する。

1) Nginx 設定の動的化

server {
    listen 8080;

    location / {
        proxy_pass http://${DIFY_VM_IP}:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;

        # Cloud Run のリクエストタイムアウト上限(最大 3600 秒)を踏まえ、要件に合わせて調整
        proxy_read_timeout 900s;
    }
}

2) デプロイスクリプト(IP 自動取得 + 公開の明示)

#!/bin/bash
set -euo pipefail

PROJECT_ID="your-project-id"
REGION="asia-northeast1"
ZONE="asia-northeast1-c"

VM_IP=$(gcloud compute instances describe dify-production \
  --zone="${ZONE}" \
  --format="get(networkInterfaces[0].accessConfigs[0].natIP)")

gcloud run deploy dify-session-buddy \
  --image="asia-northeast1-docker.pkg.dev/${PROJECT_ID}/dify-repo/dify-proxy:latest" \
  --region="${REGION}" \
  --timeout=900s \
  --set-env-vars="DIFY_VM_IP=${VM_IP}" \
  --allow-unauthenticated \
  --project="${PROJECT_ID}"

内部通信に限定する場合: --allow-unauthenticated を外し、呼び出し元に roles/run.invoker を付与したサービスアカウントで ID トークン認証を行います。さらに Ingress 制限で到達経路を絞るのがセキュアな構成です。


Phase 3: データベースバックアップの確実な自動化

課題:不正確なコマンドと手動での世代管理

gcloud compute scpVM↔ローカル間の通信を想定しており、GCSへの直接転送はできません。また、「古いバックアップをスクリプトで削除する」というアプローチは、実装が複雑で壊れやすいものでした。

回避策:VM 内で pg_dumpgcloud storage cp、世代管理は GCS ライフサイクルに一任

# 1) VM 内でバックアップを作成
gcloud compute ssh dify-production --zone=asia-northeast1-c --command \
  "cd /app/dify/docker && sudo docker-compose exec -T db pg_dump -U postgres dify > /tmp/dify_backup.sql"

# 2) VM から Cloud Storage へ直接アップロード
gcloud compute ssh dify-production --zone=asia-northeast1-c --command \
  "gcloud storage cp /tmp/dify_backup.sql gs://your-backup-bucket/"

30日で自動削除するライフサイクルルールを適用します。

cat > lifecycle.json <<'JSON'
{
  "rule": [
    { "action": { "type": "Delete" }, "condition": { "age": 30 } }
  ]
}
JSON

gcloud storage buckets update gs://your-backup-bucket --lifecycle-file=lifecycle.json

本番運用に向けた最終チェックリスト

1. セキュリティ:公開範囲を最小化する

  • 現状のリスク: 公開プロキシ経由で VM に到達可能なため、意図しないエンドポイントが露出する恐れがあります。
  • 推奨対策: Cloud Run(第2世代)であれば Direct VPC Egress を使い、Cloud Run → VPC → VM の経路に変更します。これにより、Ingress を internal に絞り、外部からの到達を完全に遮断できます。

2. 信頼性:長時間処理とタイムアウト

  • 現状の制約: Cloud Run の リクエスト上限は最大 3600 秒です。これを超える処理はタイムアウトします。
  • 推奨対策: 長時間処理は Cloud Tasks などで非同期化する設計が望ましいです。Nginx の proxy_*_timeout と Cloud Run の --timeout は、整合性を保つように設定します。

3. 運用:ログとバックアップの自動化

  • ログ: Cloud Logging の保持期間はデフォルトで30日です。環境ごとに保持期間を見直し、不要なログコストを削減しましょう。
  • バックアップ: 手動削除は廃止し、GCS ライフサイクルに一本化します。定期的なリストア演習も計画に含めると、より堅牢な運用が実現できます。

まとめ

ハッカソンという限られた時間の中で、私たちは多くの試行錯誤を繰り返しました。

  • 安価なVMは魅力的でも、可用性要件と停止仕様(Preemptible の 24h など) を理解せずに使うと本番では通用しない。
  • Cloud Run と VM のハイブリッド構成は、公開境界内部境界 を分けて設計するのが鉄則。
  • バックアップのような定型作業は、宣言的な設定(ライフサイクルなど) に移すことで、より堅牢で壊れにくい運用が実現できる。

この記事が、同様の構成を検討する方の「失敗回避マニュアル」として役立てば幸いです。


参考資料


チーム Manabi-DX-Crew

  • toki
  • M_R_K_W_
  • YF-40
  • たまちゃん
  • 座禅いぬ

Discussion