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-based | projects/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 解決策
どちらかを選ぶ。
呼び出し元(Quota Project 側)でも API を有効化する
- 最短だが、実際に使いたいプロジェクトとは別の場所で有効化が必要になり、意図が読み取りにくい
Quota Project を明示的に上書きする(推奨)
- Terraform Google Provider に
user_project_override = trueを設定する - リクエストに
X-Goog-User-Projectヘッダが付き、Quota Project がリソース作成先のプロジェクトに揃う
- Terraform Google Provider に
確認方法
実際にどのプロジェクトが Quota Project として送られているかは、HTTP のやり取りをそのまま出力させて確認できる。
gcloud <command> --log-http | grep -i x-goog-user-project
--log-http は gcloud が実際に投げているリクエスト/レスポンスのヘッダとボディを標準エラーに流すオプション。ここに出る x-goog-user-project が Quota Project の実体。