⭐️Google Cloud API 呼び出しにおける Quota Project と 403 エラーについて

Quota Project とは

API リクエストを「どのプロジェクトの利用枠(Quota)としてカウント・課金するか」を決めるプロジェクト。すべてのリクエストに必ずどこかの Quota Project が紐づく。

Quota Project が担うのは次の 3 つ。

  • Quota の消費先 … レート上限(例: 1 分あたり N リクエスト)をどのプロジェクトの枠から引くか
  • 課金先 … 有料 API の請求をどのプロジェクトに付けるか
  • API 有効化チェックの対象 … リクエストを処理する前に「そのプロジェクトで API が有効か」を確認する。無効なら実行されず 403 SERVICE_DISABLED

要するに「このリクエストの代金と枠は、どのプロジェクトのツケにするか」を決める仕組み。Google 側から見れば、支払い主が特定できないリクエストは受け付けられない。

例: Cloud Logging のログを読む

プロジェクト A のログを、プロジェクト B のサービスアカウントで読むケース。logging.googleapis.com を A で有効化しただけだと、Quota Project が B に決まった瞬間に落ちる。

403 Cloud Logging API has not been used in project B before or it is disabled.

「A のログを読みたいのに、なぜ B の話をされるのか」と混乱しがちだが、有効化チェックは**読みに行く先(A)ではなく Quota Project(B)**を見ているため。ログの中身にたどり着く前に門前払いされている。

とはいえ A 側の有効化も別途必要。公式が「有効化チェックの対象」と定めているのは Quota Project だけなので、B を有効化すれば SERVICE_DISABLED は消えるが、その先で A のリソースを触る段階が改めて失敗する(権限エラーや 404 など、API により異なる)。この 2 つは別レイヤーのチェックなので、両方で有効化しておくのが安全。

ポイントは リクエストの宛先プロジェクト(リソースの作成先)と Quota Project は別物 ということ。この 2 つがズレると 403 SERVICE_DISABLED になる。

API 種別による Quota Project 決定ルール

「2 種類の API がある」というより、Quota Project を自動で決められるかどうかで結果的に 2 つに分かれる、と捉えると分かりやすい。

  • resource-based API(Compute Engine, Cloud Storage など大部分)

    • リクエストが「どこかのプロジェクトが所有するリソース」を操作する
    • そのリソースの持ち主のプロジェクト = Quota Project に自動で決まる。呼び出し側から変更できない
  • client-based API(Cloud Translation が代表例。Discovery Engine, BigQuery Data Transfer なども該当)

    • resource-based でない API はすべてこちら(明確な定義があるわけではなく「残り」)
    • 呼び出し元の認証情報から Quota Project が決まる
    • 決定順は「リクエストで明示 → API キー → gcloud の設定 → サービスアカウントのプロジェクト → Workforce Identity Federation」

「特定のリソースに紐づかない」とは

実際のリクエストを並べると一目で分かる。リクエストのどこかに projects/○○ が書かれているかが分かれ目。

Cloud Run のサービス作成(resource-based):

POST https://run.googleapis.com/v2/projects/A/locations/asia-northeast1/services

Cloud Translation の翻訳(client-based):

POST https://translation.googleapis.com/language/translate/v2
{ "q": "こんにちは", "target": "en" }

前者は URL に projects/A があるので、Google は「A のツケだな」と即断できる。後者はどこにもプロジェクトが出てこない。「この文字列を英訳して」と言っているだけで、自分のプロジェクトにある何かを読み書きしていない(処理して返すだけで後に何も残らない)ため、そもそも書きようがない。

つまりリクエストの中に「持ち主のプロジェクト」が存在しないので、Google 側はツケの付け先を推測できない。だから呼び出し元が名乗る必要がある。名乗る手段が X-Goog-User-Project ヘッダや認証情報の quota project。

例ツケの付け先
resource-basedprojects/A の Cloud Run を作るリクエスト自身が持ち主(A)を示している
client-based文章を英訳する誰の依頼か分からない → 呼び出し元が申告する

なぜ 403 SERVICE_DISABLED になるのか

「API が有効かどうか」を判定する対象は、リソースの作成先ではなく Quota Project。

The project checked for activation is the same project checked for rate quota. — Quota project overview

つまり 403 SERVICE_DISABLED は「作成先で API が有効化されていない」ではなく「Quota Project で API が有効化されていない」というメッセージ。

  • resource-based: 作成先 = Quota Project なので、そもそもズレようがなく気づかない
  • client-based: 作成先 ≠ Quota Project になり得るので、ここで初めて表面化する

client-based API での 403 解決策

どちらかを選ぶ。

  1. 呼び出し元(Quota Project 側)でも API を有効化する

    • 最短だが、実際に使いたいプロジェクトとは別の場所で有効化が必要になり、意図が読み取りにくい
  2. Quota Project を明示的に上書きする(推奨)

    • Terraform Google Provider に user_project_override = true を設定する
    • リクエストに X-Goog-User-Project ヘッダが付き、Quota Project がリソース作成先のプロジェクトに揃う

確認方法

実際にどのプロジェクトが Quota Project として送られているかは、HTTP のやり取りをそのまま出力させて確認できる。

gcloud <command> --log-http | grep -i x-goog-user-project

--log-http は gcloud が実際に投げているリクエスト/レスポンスのヘッダとボディを標準エラーに流すオプション。ここに出る x-goog-user-project が Quota Project の実体。