コンテンツにスキップ

GCP

indx が GCP でどう動くか。1 つの Terraform スタック、手動起動のワークフロー、公開のオリジンであるグローバルロードバランサーの背後に閉じた 1 つの Cloud Run サービス、DNS で証明する Google 管理の証明書、そして変数としての Vertex AI レーン。

英語版が原文です。

1 つの Cloud Run サービスがイメージを、その run.app の URL を 閉じたまま動かします。そのためグローバルロードバランサーが唯一の入り口であり、運用者が手で作る DNS のみの 1 つの A レコードが公開ホスト名です。ここにあるものはすべて infra/gcp/.github/workflows/deploy-gcp-prod.yml です。後者は AWS のものと 同じ形の手動起動のワークフローで、変数もシークレットもアイデンティティもステートも、名前において AWS と何も共有しません (ADR-0054)。 このスタックを手で適用することは引き続き可能で、どちらでも同じスタックです。

2026-09-18 に everything.g.indx.jp で実証済みです。デプロイの実行は、公開スモーク、@deployed のシナリオ、Web アプリのスペックまで緑です。

サービスにはデータベースもバケットもなく、シークレットも一切ありません。Vertex AI には LiteLLM を 通じてインスタンス自身のサービスアカウントで到達するので、持つべき鍵がありません。重みは イメージの中にあり、オフラインで読まれます。モデルのレーンに到達するのは下のスイッチが有効な ときだけで、そうでなければ自分を利用不可と報告し、ランタイムのアイデンティティは aiplatform の ロールを持ちません。

サービスはバイト列をインラインで受け取り、それ以外は受け取りません。INDX_URI_SCHEMES=none がすべての URI ソースを無効にするので、呼び出し元はコンテナのディスク上のファイルも プロジェクト内の URL も名指しできません。第二の層として INDX_LOADER_FILE_ROOTS は サンプルのディレクトリに閉じています(ADR-0042)。AWS のタスクが AWS_REGION と並べて持つのと 同じ 3 行の固定の環境変数です。

スタック 適用するのは 持つもの
infra/gcp/terraform ワークフローの deploy、または運用者が手で 有効化する API、Artifact Registry、ランタイムのサービスアカウント、Cloud Run サービス、ロードバランサー、DNS 認可とその Google 管理の証明書、予算

ステートは運用者が先に作る Cloud Storage のバケットにあり、部分バックエンドで名指しされるので、 それについての情報はリポジトリにありません。

bucket = "indx-everything-prod-tfstate"
prefix = "indx-everything/gcp"

このステートに秘密は入りません。オリジン証明書は Google 管理で、その鍵は Google の外に出ないので、 バケットが持つのはリソースのアドレスとロックだけです。just infra::terraform::validate は資格情報なしでこのスタックをフォーマット、 バックエンドなしで初期化、検証し、.github/workflows/infra.yml がすべてのプルリクエストで それを走らせます。

境界はありません。A レコードは DNS のみなので、訪問者はバランサーに直接到達し、バランサーは 誰もを受け入れます。Cloudflare のプロキシの背後にいるもう 1 つのクラウドは AWS で、こちらは そうではありません。ホスト名がゾーンから 2 ラベル下で、Cloudflare の無料の証明書が覆うのは 1 ラベルだからであり、運用者が Cloudflare のトークンを持たないからです。このスタックが必要と する 2 つのレコードは手で作ります (ADR-0065)。 スタックは DNS のプロバイダーを何も知りません。API は認証なしで(ADR-0039)、DNS のみのレコードでは Cloudflare Access が使えません。認証は、来るとすればアプリか Google のものです。

サービスの invoker チェックは無効です(invoker_iam_disabled)。それが、バランサーに認証なしで 転送させるものです。allUsers への invoker 付与ではありません。組織のドメイン制限共有のポリシーが どの IAM ポリシーでもそのメンバーを拒み、このスイッチはメンバーを一切名指ししないからです。 ingress = INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCERrun.app の URL を閉じたままにするので、 バランサーが証明する 1 つのホスト名が、サービスが応答する唯一の名前です。

  • 運用者が所有するプロジェクトと gcloud。スタックはプロジェクトを素の変数として受け取り、 それについて何も仮定しません。ここでは専用のプロジェクトが正解です。下の 2 つの設定は プロジェクト全体に及びます。

  • Terraform >= 1.9, < 2.0

  • ステート用のバケットを一度だけ作ります。

    Terminal window
    gcloud storage buckets create gs://indx-everything-prod-tfstate \
    --project=<project id> --location=asia-northeast1 --uniform-bucket-level-access
    gcloud storage buckets update gs://indx-everything-prod-tfstate --versioning
  • ゾーンの DNS への書き込み権限。ダッシュボードで行います。資格情報は読みません。ホスト名が 必要とする 2 つのレコード、バランサーを指す A レコードと、Google が証明書を発行する対象の _acme-challenge の CNAME は手で作り、その値はスタックができて初めて存在するので、最初の デプロイはそれを印字して止まります。

スタックは必要な API を自分で有効にします。runartifactregistrycomputecertificatemanageriamloggingbillingbudgets、そしてスイッチが有効なら aiplatformcloudresourcemanager です。新しいプロジェクトでは、そうしないとどのリソースも「有効になっていない」というエラーを 返し、それは読み解くのが難しいエラーだからです。destroy では無効化しません。プロジェクトは 運用者のもので、このスタック以外を持っているかもしれないからです。

Vertex AI のクォータ自体はここでは申請しません。それはテンプレートではなくプロジェクトの 所有者のものです(ADR-0053)。

API は公開されており、Vertex AI を背後に持つレーンへの呼び出しはすべてプロジェクトに課金される ので、それらのレーンは enable_vertex_aitrue でない限り無効です。無効とは、コンテナが モデル変数を持たないことです。llmner-llmenrich-llm は宣伝されず、それを名指しする リクエストは 422 で拒否され、generic-vlmhosted-text は自分を利用不可と報告します。 スイッチが無効のままモデルを指定すると、無視されるのではなく terraform plan が失敗します。 請求先アカウントや通知先のアドレスなしでスイッチを有効にした場合も同じです。

有効にすると、これらの変数がコンテナの次の行になります。

変数 なるもの
gcp_project_id VERTEXAI_PROJECT
vertex_location VERTEXAI_LOCATION、既定では asia-northeast1。モデルの提供はリージョンごとで、サービスが動くリージョンとは別なので、独立した変数です
vertex_llm_model INDX_CLASSIFIER_LLM_MODELINDX_NER_LLM_MODELINDX_ENRICH_LLM_MODELINDX_VLM_MODEL。いずれも vertex_ai/<model>
vertex_embed_model INDX_EMBED_MODELvertex_ai/<model>
vertex_embed_dimension INDX_EMBED_DIMENSION

鍵もシークレットもありません。スイッチはランタイムのサービスアカウントに roles/aiplatform.user を加え、aiplatform.googleapis.com を有効にし、LiteLLM は資格情報を インスタンスから取ります。次元は発見されるのではなく宣言されます。次元のないホスト型の埋め込み 空間はそもそも宣伝されず、それも黙って行われるので、次元のない埋め込みモデルはプランが拒否します。

スイッチは billing_account 上の月次の Cloud Billing 予算 (vertex_monthly_budget_usd、既定では 50、整数の USD)も作り、アドレスごとに 1 つの モニタリングチャネルを通じて、予測 80 パーセントと実績 100 パーセントで alert_emails に 通知します。それはブレーキではなく警笛です。Cloud Billing の予算は AWS Budgets のように ロールを外すことができないので (ADR-0040 は AWS だけのものです)、支出を止めるのは運用者がスイッチを無効にすることであり、超過は警告から その apply までに課金された分です。予算のスコープは Vertex AI ではなくプロジェクト全体です。 Cloud Run のインスタンス、ロードバランサー、レジストリがモデルの呼び出しと一緒に数えられます。

ワークフローは鍵を持ちません。GitHub はジョブが走る Environment に対して OIDC トークンを発行し、 プロジェクトの Workload Identity Federation のプロバイダーがそれを 2 つのサービスアカウントの どちらかのトークンと交換します。それを作るのは gcloud による一度きりのコマンド列で、上の ステート用のバケットと同じく手で行います。

プールが 1 つ、プロバイダーが 1 つ、GitHub の issuer 上にあり、このワークフローが走る 2 つの subject だけを受け入れます。subject は名前ではなく不変のオーナー ID とリポジトリ ID を持つので、 リポジトリ名を変更しても信頼は移りません。AWS のロールが信頼するのと同じ形です。

Terminal window
repo="repo:INDXDev@209891251/indx-everything@1301159823"
gcloud iam workload-identity-pools create github \
--project=<project id> --location=global --display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc github \
--project=<project id> --location=global --workload-identity-pool=github \
--issuer-uri=https://token.actions.githubusercontent.com \
--attribute-mapping=google.subject=assertion.sub \
--attribute-condition="assertion.sub in ['${repo}:environment:production-gcp','${repo}:environment:production-gcp-plan']"

サービスアカウントは 2 つです。plan のアイデンティティは読み取り専用で、deploy のそれはそうでは ないからです。それぞれ自分の subject の principal だけに紐づくので、plan の Environment のために 発行されたトークンで deploy のアカウントになりすますことはできません。

Terminal window
gcloud iam service-accounts create indx-everything-prod-deploy --project=<project id>
gcloud iam service-accounts create indx-everything-prod-plan --project=<project id>
number=$(gcloud projects describe <project id> --format='value(projectNumber)')
principal="principal://iam.googleapis.com/projects/${number}/locations/global/workloadIdentityPools/github/subject"
gcloud iam service-accounts add-iam-policy-binding \
indx-everything-prod-deploy@<project id>.iam.gserviceaccount.com \
--project=<project id> --role=roles/iam.workloadIdentityUser \
--member="${principal}/${repo}:environment:production-gcp"
gcloud iam service-accounts add-iam-policy-binding \
indx-everything-prod-plan@<project id>.iam.gserviceaccount.com \
--project=<project id> --role=roles/iam.workloadIdentityUser \
--member="${principal}/${repo}:environment:production-gcp-plan"

deploy のアカウントのロールは、スタックが作るものから導いたものです。 roles/serviceusage.serviceUsageAdmin が API を有効にし、roles/artifactregistry.admin が リポジトリを作り、その IAM メンバーを設定し、イメージを push します。 roles/iam.serviceAccountAdmin がランタイムのアカウントを作り、Cloud Run のサービスがそれとして 動けるようにするのが roles/iam.serviceAccountUser です。roles/run.admin が invoker チェックを 無効にしたサービスを作ります。roles/compute.loadBalancerAdmin が NEG、バックエンド サービス、URL マップ、HTTPS プロキシ、グローバルアドレス、転送ルールを、 roles/certificatemanager.editor が DNS 認可、証明書、プロキシがそれを読むマップを、 roles/logging.admin_Default バケットの保持期間を、roles/browser が予算のフィルターが 行うプロジェクトの読み取りを受け持ちます。最後の 2 行が要るのは Vertex AI のスイッチが有効な ときだけです。ランタイムのアカウントへの roles/aiplatform.user の付与のための roles/resourcemanager.projectIamAdmin と、 予算が通知に使うチャネルのための roles/monitoring.notificationChannelEditor です。

Terminal window
for role in \
roles/serviceusage.serviceUsageAdmin \
roles/artifactregistry.admin \
roles/iam.serviceAccountAdmin \
roles/iam.serviceAccountUser \
roles/run.admin \
roles/compute.loadBalancerAdmin \
roles/certificatemanager.editor \
roles/logging.admin \
roles/browser \
roles/resourcemanager.projectIamAdmin \
roles/monitoring.notificationChannelEditor; do
gcloud projects add-iam-policy-binding <project id> \
--member="serviceAccount:indx-everything-prod-deploy@<project id>.iam.gserviceaccount.com" \
--role="$role" --condition=None
done

予算はプロジェクトではなく請求先アカウントのリソースなので、そのロールはアカウントに付けます。 スイッチが有効なときだけです。

Terminal window
gcloud billing accounts add-iam-policy-binding <billing account> \
--member="serviceAccount:indx-everything-prod-deploy@<project id>.iam.gserviceaccount.com" \
--role=roles/billing.costsManager

plan のアカウントが持つのは roles/viewer だけです。ステート用のバケットには両方のアカウントに roles/storage.objectAdmin を与えます。GCS のバックエンドは plan でもロックのオブジェクトを 書くからです。

Terminal window
gcloud projects add-iam-policy-binding <project id> \
--member="serviceAccount:indx-everything-prod-plan@<project id>.iam.gserviceaccount.com" \
--role=roles/viewer --condition=None
for account in indx-everything-prod-deploy indx-everything-prod-plan; do
gcloud storage buckets add-iam-policy-binding gs://indx-everything-prod-tfstate \
--member="serviceAccount:${account}@<project id>.iam.gserviceaccount.com" \
--role=roles/storage.objectAdmin
done

この一覧は上のリソースから導いたものであり、最初の実行までは検証されていません。最初のデプロイでの 拒否は、壊れたスタックではなく足りないロールとして読んでください。スタックはこれについて何も 仮定しません。リソースの系統ごとのカスタムロールという、より狭いポリシーを書くのは運用者です。

次に Environment を 2 つ作ります。production-gcp-plan はレビュアーなしでデプロイブランチの ルールは main のみ、production-gcp は必須レビュアーつきで同じブランチのルールです。その レビュアーが、すべての変更操作の承認です。

次にリポジトリの変数とシークレットを設定します。ワークフローが読むのはこの名前だけです。

名前 種類
GCP_PROD_DOMAIN_NAME 変数、任意 公開ホスト名。indx.jp 配下の任意の名前。既定は gcp.indx.jp
GCP_PROD_PROJECT_ID 変数 すべてのリソースを作るプロジェクト。既定値なし
GCP_PROD_WORKLOAD_IDENTITY_PROVIDER 変数 プロバイダーの完全なリソース名 projects/<number>/locations/global/workloadIdentityPools/github/providers/github。既定値なし
GCP_PROD_SERVICE_ACCOUNT 変数 deploy のアカウントのメールアドレス。既定値なし
GCP_PROD_PLAN_SERVICE_ACCOUNT 変数 plan のアカウントのメールアドレス。既定値なし
GCP_PROD_PROJECT_NAME 変数、任意 すべてのリソースの接頭辞であり、レジストリのリポジトリ名。既定は indx-everything-prod
GCP_PROD_REGION 変数、任意 サービスとレジストリが置かれるリージョン。既定は asia-northeast1
GCP_PROD_TF_BACKEND_BUCKET 変数、任意 ステート用のバケット。既定は indx-everything-prod-tfstate
GCP_PROD_TF_BACKEND_PREFIX 変数、任意 その中のプレフィックス。既定は indx-everything/gcp
GCP_PROD_MIN_INSTANCES 変数、任意 01。既定は 0
GCP_PROD_VERTEX_ENABLED 変数、任意 true で Vertex AI のレーンを有効化。既定は false
GCP_PROD_VERTEX_LLM_MODEL 変数、任意 gemini-2.5-flash。既定値なし。スイッチが無効のままならプランが拒否します
GCP_PROD_VERTEX_EMBED_MODEL 変数、任意 text-multilingual-embedding-002。既定値なし
GCP_PROD_VERTEX_EMBED_DIMENSION 変数、埋め込みモデル指定時は必須 モデルのベクトル次元数。既定は 0 で、モデルと並ぶとプランが拒否します
GCP_PROD_VERTEX_LOCATION 変数、任意 Vertex AI のリージョン。サービスが動くリージョンとは別です。既定は asia-northeast1
GCP_PROD_BILLING_ACCOUNT 変数、スイッチ有効時は必須 予算を作る請求先アカウント。例 012345-6789AB-CDEF01。既定値なし
GCP_PROD_VERTEX_MONTHLY_BUDGET_USD 変数、任意 予算が通知する整数の USD。既定は 50
GCP_PROD_ALERT_EMAILS 変数、スイッチ有効時は必須 予算が通知するアドレス。JSON のリストで、例 ["ops@example.com"]。既定は []

この一覧にシークレットは 1 つもありません。Vertex AI にはインスタンス自身のサービスアカウントで 到達し、証明書は Google のもので、このワークフローは DNS レコードを何も書きません。

GCP_PROD_DOMAIN_NAMEindx.jp 配下の任意の名前で、ホスト名がどのクラウドで動いているかを 語る必要はありません。3 つのスタックは 1 つのゾーンにレコードを書くので、どのジョブも何よりも先に、 ほかの 2 つのクラウドのホスト名である PROD_DOMAIN_NAMEAZURE_PROD_DOMAIN_NAME と等しい名前を 拒否します。2 つのワークフローが 1 つのレコードを名指さないのは、その 1 つの検査のおかげです。 deploy は運用者が confirm にその名前を打ち込んだときにだけ走ります。

すべての操作は main 上の Deploy to GCP (production) を手動で起動します。push はデプロイ しません。cutover はありません。レコードは運用者のもので、動かされることがないからです。 ロールバックのジョブもありません。ロールバックとは、レジストリがすでに持つタグのデプロイだから です。

action Environment 起きること
plan production-gcp-plan スタックの読み取り専用プラン。その要約が、デプロイが名指しすべきアーティファクトです。
deploy production-gcp タグがなければイメージを Artifact Registry へビルドし、再プランして承認済みの実行と差があれば拒否し、適用し、サービスを待ち、2 つの DNS レコードを検査して証明書を待ち、公開ホストを証明します。

どの実行も静的検証のジョブから始まります。スタックを検証し、共有のスクリプトを lint し、 ワークフローに actionlint を走らせます。本番の資格情報は使いません。続いて action が名指しする 1 つのジョブが走ります。

production-gcp-plan で走るのでレビュアーはいませんが、main からだけ、読み取りはできて書き込みは できない GCP_PROD_PLAN_SERVICE_ACCOUNT としてだけ走ります。

  1. ホスト名と、image_tag が渡されていればその形を守ります。
  2. Environment の OIDC トークンをアプリケーションのデフォルト資格情報と交換します。google プロバイダーと GCS バックエンドが読むのはそれで、そのためにほかは何も渡されません。
  3. GCP_PROD_TF_BACKEND_BUCKETGCP_PROD_TF_BACKEND_PREFIX に対して初期化し、イメージ参照 <region>-docker.pkg.dev/<project id>/<project name>/indx-everything:sha-<commit> でプランします。
  4. core-plan-summary.jsongcp-prod-plan-summary-<run id> として 7 日保持でアップロードします。 中身はリソースのアドレスとアクション、それにプランしたコミット、ref、リポジトリで、秘密は ありません。

実行 ID を控えます。それがデプロイの名指しすべき plan_run_id で、そのコミットに対してだけ 有効です。

confirm にホスト名、plan_run_id にそのプランの実行を渡して起動します。レビュアーが production-gcp Environment を承認すると、次が走ります。

  1. ref、ホスト名、confirm、数値の plan_run_idimage_tag の形を守り、その実行から承認済みの プラン要約をダウンロードします。
  2. google_artifact_registry_repository.main だけを適用し、push の前にリポジトリを存在させます。 必要な API の有効化は -target が引き連れてきます。
  3. ランナーのディスクを空け、GHCR にログインして重みのイメージを確保し、レジストリにサインインし、 Dockerfilelinux/amd64 でビルドして sha-<commit> を push します。そのタグがすでに レジストリにあればスキップします。
  4. もう一度プランし、承認済みの要約と比べます。出どころはこのリポジトリ、main、このコミットで なければならず、変更の集合は、レジストリとステップ 2 のブートストラップが有効にした API を 除いて同一でなければなりません。それ以外の差は、何も適用する前に実行を失敗させます。
  5. そのプランを適用し、Cloud Run のサービスが Ready を報告するのを最大 900 秒待ちます。
  6. ホスト名の A レコードと _acme-challenge の CNAME を解決し、バランサーのアドレスと DNS 認可の レコードと突き合わせます。違っていれば、作るべき 2 つのレコードをジョブの要約に書いて失敗 します。そこが最初の deploy の終わりです。一致していれば、証明書が ACTIVE になるのを最大 30 分待ちます。Google はチャレンジのレコードを読んだ時点で、通常は数分で発行し、オリジンには 決して到達しません。
  7. 公開スモークを走らせます。/health、各ホスト型レーンが持つべきケイパビリティ、file: の URI が拒否されること、2 つのページが配信されること、そしてバランサーのアドレスが、ホスト名を名指す 直接のクライアントに 200 を返すことを確かめます。それが公開のオリジンだからです。 min_instances = 0 では最初のリクエストはインスタンスが起きる間に失敗することが想定されるので、 スモークは何かを確かめる前に /health を最大 900 秒再試行します。
  8. ホスト名に対して just test::bdd::deployed を走らせます。運用者が手で走らせるのと同じ @deployed のシナリオです。
  9. ホスト名に対して just frontend::e2e-deployed を走らせます。UI がデモを実行できないデプロイは、 緑ではなく赤いワークフローになります。

最初の deploy は 2 サイクルです。バランサーのアドレスもチャレンジのレコードもスタックができて 初めて存在するので、最初の実行はスタックを適用し、作るべき A レコードと CNAME を自分の要約に 印字して、ステップ 6 で失敗します。それらを DNS のみで作り、もう一度 plandeploy を走らせて ください。その 2 回目の実行が証明書を待ち、3 つのスイートを走らせます。

その実行自身のプラン要約は、成否によらず gcp-prod-deploy-summary-<run id> としてアップロード されます。

image_tag に、レジストリがすでに持つイメージの sha-<commit> を、plan とその後の deploy の 両方で設定します。プランはそのイメージのプランになり、レビュアーが読むのはそれです。deploy は レジストリが持たないタグを名指しされれば、このコミットを以前のコミットの名前でビルドするのでは なく拒否します。それがロールバックです。以前のイメージを同じ承認を通して再デプロイするだけで、 レコードは決して動きません。それが指すアドレスは apply が合わせるスタックの中にあるからです。

これはワークフローが適用するのと同じスタックで、上の GitHub の設定を持たない運用者のための 手順です。サインインします(Terraform はアプリケーションのデフォルト資格情報を読み、gcloud auth login だけでは それは書かれません)。

Terminal window
gcloud auth login
gcloud auth application-default login
gcloud config set project <project id>

2 つの例からバックエンドと変数を書き、初期化します。

Terminal window
cd infra/gcp/terraform
cp backend.hcl.example backend.hcl
cp terraform.tfvars.example terraform.tfvars
terraform init -backend-config=backend.hcl

イメージを push する前にレジストリが存在していなければならず、Cloud Run のサービスはイメージ なしでは起動できないので、レジストリだけを先に適用します。必要な API の有効化は -target が 引き連れてきます。

Terminal window
terraform apply -target=google_artifact_registry_repository.main

イメージをビルドし、terraform.tfvars が名指しするタグで push します。

Terminal window
just infra::image::build
tag="sha-$(git rev-parse HEAD)"
repo="asia-northeast1-docker.pkg.dev/<project id>/indx-everything-prod/indx-everything"
docker tag indx-everything:local "$repo:$tag"
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
docker push "$repo:$tag"

残りをスイッチ無効のまま適用して、モデルには何も課金されないようにし、レコードに必要な 2 つの値を 読みます。

Terminal window
terraform apply
terraform output -raw load_balancer_ip
terraform output -json acme_challenge_record

ゾーンにその 2 つのレコードを DNS のみで作ります。ホスト名に、アドレスを内容とする A レコードと、 チャレンジのレコードの name に、その data を内容とする CNAME です。

Google は _acme-challenge のレコードを読み、その後の数分でそれに対して証明書を発行するので、 それまでロードバランサーは 443 で何も応答しません。ACTIVE になるのを見届けます。

Terminal window
gcloud certificate-manager certificates describe indx-everything-prod-origin \
--project=<project id> --format='value(managed.state)'

そのうえで、ホスト名は応答しなければならず、クライアントがホスト名を名指すならアドレス自身も 応答しなければなりません。バランサーが公開のオリジンだからです。

Terminal window
curl -sf https://<hostname>/health | jq -e '.status == "ok"'
curl -s -o /dev/null -w '%{http_code}\n' \
--resolve "<hostname>:443:$(terraform output -raw load_balancer_ip)" \
https://<hostname>/health

続いてホストの残りを確かめます。

Terminal window
curl -sf https://<hostname>/ | grep -q '<html'
curl -sf https://<hostname>/en/deploy/ | grep -q '<html'

そしてサービスを一度温め、実ホストを叩く 2 つのスイートを走らせます。

Terminal window
curl -sf https://<hostname>/health >/dev/null
just test::bdd::deployed https://<hostname>
just frontend::e2e-deployed https://<hostname>

この温めは儀式ではありません。下の節を見てください。最後にスイッチを有効にしてもう一度適用します。 terraform.tfvars には enable_vertex_ai = truebilling_account、モデル、次元、alert_emails を設定します。

Terminal window
terraform apply

スイッチをどちらの向きに切り替えても、コンテナの環境が変わるので新しいリビジョンになります。

min_instances は既定で 0、検証で 0 か 1 に限られます。max_instance_count はどちらの場合も 1 です。0 はアイドルの間に何も費やしません。1 はインスタンスを 1 つ温めたままにし、24 時間 課金されます。

0 に代償がないわけではありません。コールドスタートはイメージの pull とエンジンの import であり、 起動プローブはそれに 240 秒(10 秒 × 24 回、Cloud Run の上限)を与えます。Cloud Run 自身の リクエストのタイムアウト(request_timeout_seconds、既定では 120)はリクエストが届いた時点から 測られるので、冷えたインスタンスを待つ時間も区切ります。それを超える起動は、プローブにまだ時間が 残っていても 504 になります。したがってアイドルの後の最初のリクエストは失敗することが想定され、 1 分後のリクエストは、それが起こしたインスタンスに対して成功します。スクリプトから呼ぶクライアントは その最初の失敗を許容しなければならず、許容できないなら min_instances = 1 がその設定です。

コールドスタートは実際のデプロイではまだ計測していません。最初のデプロイの後にここへ数字を 書き入れてください。

  • どちらのレコードもプロキシしないままにする。 Google は証明書を発行し更新するために _acme-challenge.<hostname> を解決し、そのレコードはその名前で唯一のレコードでなければ なりません。プロキシされた A レコードは、無料の証明書が覆わないホスト名の前に Cloudflare の エッジを置くことになり、ブラウザはバランサーに届く前にハンドシェイクで失敗します。indx.jp に CAA レコードがあれば pki.goog を許可しなければなりません。今はありません。
  • 証明書はレコードができたあとに発行される。 DNS 認可は非同期です。Google はチャレンジの レコードを読んでから、通常は数分で発行します。見るべきは証明書の managed.state で、 ワークフローはスモークの前にそれを待ちます。それまでロードバランサーは 443 で何も応答しません。
  • レコードを突き合わせるステップは DNS をランナーのリゾルバーで読む。 実行の少し前に作った レコードはそれでもそこにないことがあり、そのときステップは、レコードが存在しないかのように それを印字して失敗します。1 分待ってもう一度起動してください。直すものはありません。
  • _Default のログバケットの保持期間はプロジェクト全体に及ぶ。 log_retention_days の行き先は ほかにありません。Cloud Run は Cloud Logging に書き、サービスごとの保管場所はないからです。 共有プロジェクトでは、これが黙って別のワークロードの保持期間を変えます。専用のプロジェクトが 本当の答えです。
  • cpu_idle = false はリクエストではなくインスタンスの生存時間全体に課金する。 エンジンが 初回利用時にキャッシュを温め、絞られたインスタンスではそれが際限なく遅くなるため、CPU は リクエストの間も割り当てられたままです。min_instances = 1 では、それが container_cpu (既定では 2 vCPU)を 24 時間課金することになります。
  • 予算が見ているのはプロジェクトであってモデルではない。 Vertex AI のサービスに絞るのは budget_filter.services の項目であり、このテンプレートがしない仕事です。
  • monitoring.googleapis.com は有効化の一覧にない。 新しいプロジェクトでは既定で有効です。 無効になっているプロジェクトでは、スイッチを有効にしたときに通知チャネルで失敗します。 cloudresourcemanager.googleapis.com は一覧にありますが、スイッチを有効にしたときだけです。 予算のフィルターはプロジェクト番号を受け取り、それは Cloud Resource Manager の読み取りです。 その読み取りの count が、スイッチが無効の間は読み取り自体をなくし、depends_on がそれを apply まで留めるので、スイッチを有効にした最初の plan が、まだ有効化されていない API を呼ぶ ことはありません。
  • deletion_protectionfalse google プロバイダーの既定は true で、そのままだと terraform destroy は、それを消すための apply を挟むまで失敗します。1 人の運用者が手で適用し 手で壊すスタックでは、それは守りではなく罠です。ここで destroy を止めるのは運用者だけです。