コンテンツにスキップ

GCP

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

英語版が原文です。

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

サービスにはデータベースもバケットもなく、シークレットも一切ありません。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 サービス、ロードバランサーとその Cloud Armor ポリシー、DNS 認可とその Google 管理の証明書、2 つの Cloudflare レコード、予算

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

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

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

境界は Cloud Armor のポリシーの 2 つのルールで、優先度の昇順に評価されます。優先度 900 の ルールは Host ヘッダーが domain_name に一致しないとき 403 で拒否します。先頭と末尾を固定し、 大文字小文字を区別せず、ポートは任意です。これが、別のゾーンがこのアドレスを指して自分のものと して配信するのを止めます。優先度 1000 からのルールが Cloudflare の公開 IPv4 レンジを許可します。 1 ルールあたり 10 レンジなのは、それが Cloud Armor の 1 ルールの上限だからです。最後のルールが それ以外のすべてを拒否します。それは境界であって認証ではありません。API はその背後で認証なしで あり、Cloudflare の顧客は誰もがそのレンジの内側にいます(ADR-0039)。ホスト名の Cloudflare Access が、運用者が有効にする門です。

サービスの allUsers の invoker 付与はアプリを公開するものではありません。バランサーは認証なしで 転送するので、サービスは匿名の呼び出し元を受け入れる必要があるだけです。門は ingress = INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER で、これがバランサーだけを受け入れます。

  • 運用者が所有するプロジェクトと 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
  • 1 つの Cloudflare の資格情報。環境から読まれます。ゾーンの DNS を編集する CLOUDFLARE_API_TOKEN です。このスタックが Cloudflare に求めるのは 2 つのレコードだけで、 証明書は Google のものです。

スタックは必要な 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 がサービスを作り、 allUsers に invoker のロールを与えます。roles/compute.loadBalancerAdmin が NEG、バックエンド サービス、URL マップ、HTTPS プロキシ、グローバルアドレス、転送ルールを、 roles/certificatemanager.editor が DNS 認可、証明書、プロキシがそれを読むマップを、 roles/compute.securityAdmin が Cloud Armor のポリシーを、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/compute.securityAdmin \
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_CLOUDFLARE_ZONE_ID 変数、任意 レコードを書くゾーン。既定は indx.jp のゾーン
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"]。既定は []
GCP_PROD_CLOUDFLARE_API_TOKEN シークレット、任意 ゾーンの DNS を編集できるトークン。未設定ならワークフローは PROD_CLOUDFLARE_API_TOKEN、次に AWS のワークフローが持つ CLOUDFLARE_API_TOKEN を読みます。ゾーンは 1 つのゾーンだからです

この一覧にモデルの鍵はなく、証明書の鍵もありません。Vertex AI にはインスタンス自身のサービス アカウントで到達し、証明書は Google のものなので、このデプロイが持つシークレットは Cloudflare の DNS トークン 1 つだけです。

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 はありません。Cloudflare のレコードはスタックの中にあるので、ホスト名を 生かすのは最初のデプロイです。ロールバックのジョブもありません。ロールバックとは、レジストリが すでに持つタグのデプロイだからです。

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

どの実行も静的検証のジョブから始まります。スタックを検証し、共有のスクリプトを 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> でプランします。 プランは Cloudflare の IP 範囲とゾーンのレコードを読むので、DNS トークンを持つのはそのためです。
  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 秒待ちます。apply は _acme-challenge のレコードも書き、Google は apply が返ったあとにそれに対して証明書を発行する ので、その数分のあいだロードバランサーは 443 で何も応答しません。
  6. Cloudflare 経由で公開スモークを走らせます。/health、各ホスト型レーンが持つべきケイパビリティ、 file: の URI が拒否されること、2 つのページが配信されること、そしてロードバランサーの アドレスに対するよその Host が 403 であることを確かめます。最後のものが、Cloud Armor の Host の式に対する唯一の証明です。min_instances = 0 では最初のリクエストはインスタンスが 起きる間のプロキシの 524 に、初回のデプロイでは証明書の発行中の 5xx になることが想定される ので、スモークは何かを確かめる前に /health を最大 900 秒再試行します。
  7. ホスト名に対して just test::bdd::deployed を走らせます。運用者が手で走らせるのと同じ @deployed のシナリオです。
  8. ホスト名に対して just frontend::e2e-deployed を走らせます。UI がデモを実行できないデプロイは、 緑ではなく赤いワークフローになります。

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

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

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

Terminal window
gcloud auth login
gcloud auth application-default login
gcloud config set project <project id>
export CLOUDFLARE_API_TOKEN=<ゾーンの DNS を編集できるトークン>

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"

残りをスイッチ無効のまま適用します。最初のデプロイが境界を証明し、モデルには何も課金されません。

Terminal window
terraform apply
terraform output

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

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

Cloud Armor の Host の式は apply の前には何も証明できないので、apply の直後にすることは 2 つの curl です。ホスト名は応答しなければならず、同じアドレスに対するそれ以外の Host は 403 で なければなりません。

Terminal window
curl -sf https://<hostname>/health | jq -e '.status == "ok"'
curl -ks -o /dev/null -w '%{http_code}\n' \
--resolve "other.example:443:$(terraform output -raw load_balancer_ip)" \
https://other.example/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 の上限)を与えますが、Cloudflare の プロキシは 100 秒で 524 を返して諦めます。Cloud Run 自身のリクエストのタイムアウト (request_timeout_seconds、既定では 120)はリクエストが届いた時点から測られるので、冷えた インスタンスを待つ時間も区切ります。それを超える起動は、プローブにまだ時間が残っていても 504 に なります。したがってアイドルの後の最初のリクエストはプロキシで失敗することが想定され、1 分後の リクエストは、それが起こしたインスタンスに対して成功します。スクリプトから呼ぶクライアントは その最初の失敗を許容しなければならず、許容できないなら min_instances = 1 がその設定です。

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

  • Cloud Armor の Host の式を証明するのはその 2 つの curl だけ。 受け付けられない CEL の式は plan ではなく apply での 400 であり、間違った式はすべての呼び出し元を拒否します。最初の apply の 後と、domain_name を変えたあとに両方の curl を走らせてください。アドレスへ直接の curl は Host のルールだけでなく送信元レンジのルールにも拒否されるので、それが確かめるのはポリシーが 効いていることであって 1 つのルールを切り分けることではありません。切り分けるのは、このアドレスを 指す同じゾーンのもう 1 つのプロキシされたレコードです。
  • 証明書は apply が返ったあとに発行される。 DNS 認可は非同期です。apply は _acme-challenge.<hostname> を書いて返り、Google はそのレコードを読んでから、通常は数分で 発行します。見るべきは証明書の managed.state で、ワークフローのスモークはその間を再試行で しのぎます。そのレコードはその名前で唯一のレコードでなければならず、Google が解決するので プロキシしません。indx.jp に CAA レコードがあれば pki.goog を許可しなければなりません。 今はありません。
  • run.app の URL は意図して閉じている。 ingress = INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER こそが Cloud Armor を境界たらしめています。何かを試すために開けることは、それがあるから安全な allUsers の invoker 付与も含め、境界全体を迂回することです。
  • _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 を止めるのは運用者だけです。
  • 境界は IPv4 のみで、ALB のセキュリティグループと同じです。IPv6 の訪問者は Cloudflare が 受け、オリジンへは IPv4 で到達します。