はじめに
こんにちは、@_mkazutakaです。
今回は、Terraform + Cloud Runを使ったDifyの環境構築と運用についてTerraformのコード等を交えて紹介します。 Cloud Runを採用することで、土日や深夜帯などアクセスがない時間帯のコストを最小限に抑えれたり、VM上で運用するより管理コストを下げることができます。 社内環境にはうってつけですね。
環境構築の前に
Cloud Runを利用したDifyの環境構築については、すでにkotamatさんがZennでスクラップ記事を投稿されています。 /zenn.dev
これを参考に、以下のサービスを利用します。
- DBには、PostgreSQLのBaaSであるSupabase
- インメモリDBには、RedisのBaaSであるUpstash
- メールの送信には、Resend
ユーザを招待する際に招待メールを送信するのですが、その際、CloudRunだとSMTPを使ったメール送信ができません。幸いにもDifyは、ResendのAPIを使ってメール送信も行えるため、こちらを利用します。
Difyのコンポーネント
構築の前に、Difyにはどういったコンポーネントがあるのか簡単に紹介します。 詳細に関しては、エムスリーさんの記事がわかりやすくまとめられているのでご参照ください。
本記事では、以下の4つのコンポーネントをデプロイします。
- api
- web
- worker
- sandbox
それぞれをどういうアーキテクチャでデプロイするかは悩みどころですが、弊社では3つのCloud Runリソースを作り、以下の形で構成しています。
またnginxは使わず、Cloud Load Balancingを使っています。
環境構築をする
Difyを構築するためには、30個ほど環境変数の設定が必要になります。。 少し長くなるのですが、上記のSaaSを使った環境変数の設定とそれを使ったTerraformでの構築を紹介していきます。
ステップとしては以下のようになります
- SupabaseからPostgreSQL用の環境変数の値を取得
- SupabaseからVectorDB用の環境変数の値を取得
- UpstashからRedis用の環境変数の値を取得
- Resendからメール送信用の環境変数の値を取得
- Cloud Storageからモデル保存用の 環境変数の値を取得
- デプロイ用のTerraformのコードを書く
1. SupabaseからPostgreSQL用の環境変数の値を取得
Supabaseへのログイン後、プロジェクトを選択、左サイドバーのProject SettingsからCONFIGURATION->Databaseを選択すると必要な要素が出てくると思います。 DB_PASSWORDのみプロジェクトに作成する際に入力した値になります。
Supabaseのページが以下の図の場合、値はそれぞれ以下のようになります
{ name = "DB_USERNAME", value = "postgres.***" }, { name = "DB_PASSWORD", value = "***" }, { name = "DB_HOST", value = "aws-0-ap-northeast-1.pooler.supabase.com" }, { name = "DB_PORT", value = "6543" }, { name = "DB_DATABASE", value = "postgres"},
2. SupabaseからVectorDB用の環境変数の値を取得
必要な情報は、Supabaseの設定とほぼ同じですが、ベクトルDBとしても使う場合、pgvector拡張を有効にする必要があります。 Supabaseのサイドバーから「Database」->「DATABASE MANEGEMENT」->「Extensions」を選択すると拡張一覧が表示されるので、"pgvector"と入力し、「vector」拡張をONにしてください
VectorDBまわりの環境変数はステップ1と合わせて以下のようになります。
{ name = "VECTOR_STORE", value = "pgvector" }, # pgvectorを利用するので`pgvector`にする { name = "PGVECTOR_HOST", value = "aws-0-ap-northeast-1.pooler.supabase.com" }, { name = "PGVECTOR_PORT", value = "6543" }, { name = "PGVECTOR_USER", value = "postgres.***" }, # Step 1のDB_USERNAMEと同じ { name = "PGVECTOR_PASSWORD", value = "***" }, # Step 1のDB_PASSWORDと同じ { name = "PGVECTOR_DATABASE", value = "postgres" },
3. UpstashからRedis用の環境変数の値を取得
Upstashへのログイン後、DATABASESからデータベースを選択すると、必要な要素が出てくると思います。 Databaseページが以下の図の場合、値はそれぞれ以下のようになります。 SSL接続するため、CELERY_BROKER_URLは、redisではなくredissにすることに注意してください。
Upstashのページが以下の図の場合、値はそれぞれ以下のようになります。
{ name = "REDIS_HOST", value = "picked-***.upstash.io" }, { name = "REDIS_PORT", value = "6379" }, { name = "REDIS_USERNAME", value = "default" }, # デフォルト値が 'default' { name = "REDIS_PASSWORD", value = "***" }, { name = "REDIS_USE_SSL", value = "true" }, # SSLを使うので`true` { name = "REDIS_DB", value = "0" }, { name = "CELERY_BROKER_URL", value = "rediss://defualt:<PASSWORD>@picked-***.upstash.io:6379/0" }, # redis`s`に注意
4. Resendからメール送信用の環境変数の値を取得
招待メールを送信が不要ならおそらくこのステップは不要です。 Resendへのログイン後、ドメインの追加とAPIキーの発行をしてください。
{ name = "MAIL_TYPE", value = "resend" }, { name = "RESEND_API_KEY", value = "***" }, { name = "RESEND_API_URL", value = "https://api.resend.com" }, { name = "MAIL_DEFAULT_SEND_FROM", value = "noreply@***"}, # 設定したドメインならなんでもいいはず
5. Cloud Storageからモデル保存用の 環境変数の値を取得
Cloud Runを使うためローカルのストレージ領域は使えません。 ストレージの指定がないと、モデルのAPIキーがうまく保存されないためストレージに保存する必要があります。
ストレージ作成からサービスアカウントキー取得までのステップは以下のようになります。
- GCP上からGCSのバケットを作成(dify-storage-***)します。
- その後、サービスアカウントを作成、ロールは「Storage オブジェクト ユーザー」をつけます。
- サービスアカウント用の鍵を作成、ダウンロードし、その値をbase64にします。
これらを使った値は以下のようになります。
{ name = "STORAGE_TYPE", value = "google-storage" }, { name = "GOOGLE_STORAGE_BUCKET_NAME", value = "dify-storage-***" }, { name = "GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64", value = "***" },
6. デプロイ用のTerraformのコードを書く
初期設定を以下のようにします。
locals { project = "***" # GCPのプロジェクト名 region = "asia-northeast1" host = "xxx.example.com # ローカルDifyのドメイン dify_version = "0.8.0" dify_sandbox_version = "0.2.7" } provider "google" { project = local.project region = local.region }
次にCloud Load Balancingの設定です。 apiとwebへのリクエストを外部からリクエストできるようにしています。
################################## # Web Network ################################## resource "google_compute_region_network_endpoint_group" "dify-web-neg" { name = "dify-web-neg" network_endpoint_type = "SERVERLESS" region = local.region cloud_run { service = google_cloud_run_v2_service.dify-web.name } } resource "google_compute_backend_service" "dify-web-backend-service" { name = "dify-web-backend-service" load_balancing_scheme = "EXTERNAL" protocol = "HTTP" backend { group = google_compute_region_network_endpoint_group.dify-web-neg.id } } ################################## # API Network ################################## resource "google_compute_region_network_endpoint_group" "dify-api-neg" { name = "dify-api-neg" network_endpoint_type = "SERVERLESS" region = local.region cloud_run { service = google_cloud_run_v2_service.dify-api.name } } resource "google_compute_backend_service" "dify-api-backend-service" { name = "dify-api-backend-service" load_balancing_scheme = "EXTERNAL" protocol = "HTTP" backend { group = google_compute_region_network_endpoint_group.dify-api-neg.id } } resource "google_compute_url_map" "dify_url_map" { name = "dify-url-map" default_service = google_compute_backend_service.dify-web-backend-service.self_link host_rule { hosts = [local.host] path_matcher = "path-matcher" } path_matcher { name = "path-matcher" default_service = google_compute_backend_service.dify-web-backend-service.self_link path_rule { paths = ["/console/api/*", "/api/*", "/v1/*", "/files/*"] service = google_compute_backend_service.dify-api-backend-service.self_link } path_rule { paths = ["/"] service = google_compute_backend_service.dify-web-backend-service.self_link } } } module "lb-http" { source = "GoogleCloudPlatform/lb-http/google//modules/serverless_negs" version = "~> 11.0" project = local.project name = "dify-lb" ssl = true managed_ssl_certificate_domains = [local.host] https_redirect = true backends = { default = { protocol = "HTTP" port_name = "http" enable_cdn = false log_config = { enable = true sample_rate = 1.0 } groups = [ { group = google_compute_region_network_endpoint_group.dify-web-neg.id } ] iap_config = { enable = false } } } create_url_map = false url_map = google_compute_url_map.dify_url_map.self_link }
次にAPI側のコンテナ環境を作ります。
locals { env_vars = [ { name = "CONSOLE_WEB_URL" value = "https://${local.host}" }, # ツール一覧の画像表示に必要だったりする { name = "CONSOLE_API_URL", value = "https://${local.host}" }, { name = "APP_WEB_URL", value = "https://${local.host}" }, # セッションクッキーを安全に署名し、データベース上の機密情報を暗号化するためのキー。 # `openssl rand -base64 42`で作成 { name = "SECRET_KEY", value = "" }, # 上記で取得した値 { name = "DB_USERNAME", value = "" }, { name = "DB_PASSWORD", value = "" }, { name = "DB_HOST", value = "" }, { name = "DB_PORT", value = "6543" }, { name = "DB_DATABASE", value = "postgres" }, { name = "MIGRATION_ENABLED", value = "false" }, # 上記で取得した値 { name = "REDIS_HOST", value = "" }, { name = "REDIS_PORT", value = "" }, { name = "REDIS_USERNAME", value = "default" }, { name = "REDIS_PASSWORD", value = "" }, { name = "REDIS_USE_SSL", value = "true" }, { name = "REDIS_DB", value = "0" }, { name = "CELERY_BROKER_URL", value = "" }, # 上記で取得した値 { name = "MAIL_TYPE", value = "resend" }, { name = "RESEND_API_KEY", value = "" }, { name = "RESEND_API_URL", value = "https://api.resend.com" }, # Resend上で設定したドメインを使った適当なメールアドレス { name = "MAIL_DEFAULT_SEND_FROM", value = "noreply@" }, # 上記で取得した値 { name = "STORAGE_TYPE", value = "google-storage" }, { name = "GOOGLE_STORAGE_BUCKET_NAME", value = "" }, { name = "GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64", value = "" }, # 上記で取得した値 { name = "VECTOR_STORE", value = "pgvector" }, { name = "PGVECTOR_HOST", value = "" }, { name = "PGVECTOR_PORT", value = "6543" }, { name = "PGVECTOR_USER", value = "" }, { name = "PGVECTOR_PASSWORD", value = "" }, { name = "PGVECTOR_DATABASE", value = "postgres" }, { name = "CODE_EXECUTION_ENDPOINT", value = google_cloud_run_v2_service.dify-sandbox.uri }, # CODE_EXECUTION間の認証で使用するキー。自分で生成する { name = "CODE_EXECUTION_API_KEY", value = "" }, # 必要に応じて { name = "LOG_LEVEL", value = "INFO" }, { name = "DEBUG", value = "true" }, { name = "SENTRY_DSN", value = "" }, ] } resource "google_cloud_run_v2_service" "dify-api" { name = "dify-api" location = local.region project = local.project template { session_affinity = "true" max_instance_request_concurrency = 1000 timeout = "3600s" execution_environment = "EXECUTION_ENVIRONMENT_GEN2" scaling { max_instance_count = 1 min_instance_count = 1 } containers { name = "api" image = "langgenius/dify-api:${local.dify_version}" resources { limits = { cpu = "1" memory = "2Gi" } cpu_idle = "true" startup_cpu_boost = "true" } ports { container_port = 5001 } startup_probe { tcp_socket { port = 5001 } failure_threshold = 1 period_seconds = 240 timeout_seconds = 240 } dynamic "env" { for_each = local.env_vars content { name = env.value.name value = env.value.value } } env { name = "MODE" value = "api" } } containers { name = "worker" image = "langgenius/dify-api:${local.dify_version}" resources { limits = { cpu = "2" memory = "2Gi" } cpu_idle = "false" startup_cpu_boost = "true" } dynamic "env" { for_each = local.env_vars content { name = env.value.name value = env.value.value } } env { name = "MODE" value = "worker" } } } } resource "google_cloud_run_service_iam_member" "dify-api-member" { location = google_cloud_run_v2_service.dify-api.location project = google_cloud_run_v2_service.dify-api.project service = google_cloud_run_v2_service.dify-api.name role = "roles/run.invoker" member = "allUsers" }
次にWEBとのコンテナを作ります
resource "google_cloud_run_v2_service" "dify-web" { name = "dify-web" location = local.region project = local.project template { session_affinity = "true" max_instance_request_concurrency = 1000 scaling { max_instance_count = 1 min_instance_count = 0 } containers { name = "web" image = "langgenius/dify-web:${local.dify_version}" ports { container_port = 8080 } resources { limits = { cpu = "2" memory = "2Gi" } cpu_idle = "true" startup_cpu_boost = "true" } startup_probe { tcp_socket { port = 8080 } failure_threshold = 1 period_seconds = 240 timeout_seconds = 240 } env { name = "CONSOLE_API_URL" value = "https://${local.host}" } env { name = "APP_API_URL" value = "https://${local.host}" } env { name = "SENTRY_DSN" value = "" } } } } resource "google_cloud_run_service_iam_member" "dify-web-member" { location = google_cloud_run_v2_service.dify-web.location project = google_cloud_run_v2_service.dify-web.project service = google_cloud_run_v2_service.dify-web.name role = "roles/run.invoker" member = "allUsers" }
最後にSandboxのコンテナを作ります
resource "google_cloud_run_v2_service" "dify-sandbox" { name = "dify-sandbox" location = local.region project = local.project template { session_affinity = "true" max_instance_request_concurrency = 1000 scaling { max_instance_count = 1 min_instance_count = 0 } containers { name = "sandbox" image = "langgenius/dify-sandbox:${local.dify_sandbox_version}" ports { container_port = 8194 } resources { limits = { cpu = "2" memory = "2Gi" } cpu_idle = "true" startup_cpu_boost = "true" } startup_probe { tcp_socket { port = 8194 } failure_threshold = 1 period_seconds = 240 timeout_seconds = 240 } env { name = "API_KEY" value = "" } env { name = "GIN_MODE" value = "release" } env { name = "WORKER_TIMEOUT" value = "100" } env { name = "SANDBOX_PORT" value = "8194" } } } } resource "google_cloud_run_service_iam_member" "dify-sandbox-member" { location = google_cloud_run_v2_service.dify-sandbox.location project = google_cloud_run_v2_service.dify-sandbox.project service = google_cloud_run_v2_service.dify-sandbox.name role = "roles/run.invoker" member = "allUsers" }
以上で完了です。お疲れ様でした!
運用
運用するうえでいくつかのTipsも紹介します。
DIfyのバージョンを上げる
Terraform上のdify_versionとdify_sandbox_versionを変更し、terraform applyすれば終わりです。
DBマイグレーション
バージョンが上がるとデータベースのマイグレーションをする必要があるケースがあります
APIのコンテナの環境変数に、MIGRATION_ENABLED
があるこれをtrue
にすればコンテナの起動時にマイグレーションを実行してくれるっぽいのですが、弊社の環境(CloudRunかな)だと初回はうまくいくが、バージョンアップ時にうまく動かなかったので手動でマイグレーションするようにしています。
マイグレーション方法は以下の手順で行います。
$ git clone https://github.com/langgenius/dify $ cd dify/api $ poetry install # データベース環境変数の設定 $ vim .env #> DB_USERNAME= #> DB_PASSWORD= #> DB_HOST= #> DB_PORT=6543 #> DB_DATABASE= $ poetry run dotenv run flask db current
その他
Terraformのコードを見てもらえるとわかるのですが、apiだけmin_instance_countを1
に設定しています。
コンテナが0の状態だと、アクセスした際に1回でうまくページが表示されないためです。(一度webページにアクセスし、しばらく後に更新すると表示されます)
最後に
少し長くなりましたが、Dify環境の構築に役立つと幸いです。