↓メインコンテンツへスキップ
  1. Blogs/

TerraformでGoogle Cloudの予算アラートを作り、Pub/Sub通知の中身まで確かめた

4 分
Terraform GoogleCloud CloudBilling PubSub
0222-nnn
著者
0222-nnn
猫が好き
目次
Terraform-GoogleCloud - この記事は連載の一部です
パート 39: この記事

概要
#

Cloud Billingの予算は、コスト管理の入口としてよく紹介されます。ところが設定しようとすると、いくつも引っかかります。

APIが無効で一覧すら取れません。terraform applyは403で通りません。Pub/Sub通知のために権限を付けろと書かれているサービスアカウントは、そもそも名前が違っていました。

そして最も大事な点があります。この記事で作る通知だけの予算(alerts-only budget)は、課金を止めません。

2026年7月27日に Spend Cap Budget が Preview で提供開始されました。対象サービスなら、新規の利用を自動停止できます。

対象サービスは4つです。

  • Gemini API
  • Gemini Enterprise Agent Platform
  • Cloud Run
  • Cloud Run functions

ただし、確かめた範囲では Terraform から作れませんでした。理由はあとで実際に確かめます。

上限に達しても通知が飛ぶだけで、リソースは動き続けます。「予算を設定したから安心」にはなりません。

元にしたPoCは通知を請求アカウントの既定メールに任せていて、届くかどうかを確かめていませんでした。この記事ではPub/Subに送り、メッセージを実際に読みます。

対象読者は、TerraformでGoogle Cloudのリソースを作ったことがある人です。請求周りの入門は扱いません。

通知を受けてリソースを停止する実装、組織レベルの予算、BigQueryへの課金データエクスポートは扱いません。

検証すること
#

  • User ADC から Budget API を使うのに何が要るのか
  • プロジェクトの指定は ID か番号か
  • Pub/Sub 通知は本当に届くのか
  • 通知のメッセージには何が入るのか
  • Publisher の権限は誰が付けるのか
  • 通常の予算を超えたら何が起きるのか
  • Spend Cap Budget は Terraform で作れるのか
  • 予算の操作は監査ログで追えるのか

前提環境
#

  • Google Cloud CLI、Terraform
  • 請求アカウントに対するroles/billing.adminまたはroles/billing.costsManager
  • 通知先トピックのpubsub.topics.setIamPolicy(Budget API が Publisher ロールを付けるのに要る)。ドメイン制限共有を有効にしている組織では、あわせて除外設定も要る
  • Pub/Sub のトピックとサブスクリプションを作成・削除・受信する権限
  • User ADC を使う場合、billing_project に指定するプロジェクトのserviceusage.services.use
  • billing_project に指定するプロジェクトで Budget API(billingbudgets.googleapis.com)を有効化しておくこと
  • 検証時のバージョン:Terraform 1.14.5、google 7.46.0

今回は請求アカウントの権限で作成します。プロジェクト1つを対象にする予算なら、プロジェクト側の権限でも作成できます(resourcemanager.projects.get / billing.resourcebudgets.read / billing.resourcebudgets.write)。

使用するTerraformコード
#

以降の gcloud はリソース名を短く書いています。CLI の参照先を、Terraform の project_id と揃えてから実行してください。

gcloud config set project YOUR_PROJECT_ID

構成
#

flowchart LR
    BA["請求アカウント"] --> B["予算
上限 1 JPY"] P["プロジェクトの課金"] -.->|"budget_filter"| B B -->|"all_updates_rule"| T["Pub/Sub トピック"] T --> S["サブスクリプション
pull"]

VMもクラスタも作りません。Pub/Subのトピックとサブスクリプションだけなので、ほぼ無料です。

予算は請求先アカウント配下のリソース
#

まずここでつまずきます。予算はプロジェクトに属していません。

gcloud billing budgets list --billing-account=BILLING_ACCOUNT_ID \
  --filter="displayName:tf-adv" --format="value(name,displayName)"
billingAccounts/BILLING_ACCOUNT_ID/budgets/3198661d-...	tf-adv-budget-pubsub

リソース名がbillingAccounts/で始まります。プロジェクトを削除しても予算は残ります。 gcloud projects系のコマンドでは一切見えません。

destroyの確認も、terraform state listが0件では足りず、請求アカウント側で見る必要があります。

User ADC では provider に user_project_override が要る
#

terraform applyが通らないところから始まりました。

Error creating Budget: googleapi: Error 403: Your application is authenticating
by using local Application Default Credentials. The billingbudgets.googleapis.com
API requires a quota project, which is not set by default.

gcloud auth application-default set-quota-projectを実行しても同じエラーです。providerに書く必要があります。

provider "google" {
  project = var.project_id
  region  = var.region

  user_project_override = true
  billing_project       = var.project_id
}

User ADC で Budget API を呼ぶなら、API のクォータプロジェクトを明示する必要があります。 Terraform の公式資料も、User ADC では user_project_override = true と billing_project の両方を求めています。

billing_project はクォータの請求先であって、予算が属する請求アカウントや、予算が集計する費用の対象とは別です。

budget_filter.projects は「番号」
#

project IDではなくprojects/<番号>の形式です。

data "google_project" "scope" {
  project_id = var.project_id
}

resource "google_billing_budget" "main" {
  budget_filter {
    projects = ["projects/${data.google_project.scope.number}"]
  }
}

Publisher権限は自分で付けなくても通知が届いた
#

Pub/Sub通知の手順として、請求のサービスエージェントにroles/pubsub.publisherを付けるという説明をよく見ます。

gcloud pubsub topics add-iam-policy-binding tf-adv-budget-notifications \
  --member="serviceAccount:billing-budgets@system.gserviceaccount.com" \
  --role=roles/pubsub.publisher
ERROR: INVALID_ARGUMENT: Service account billing-budgets@system.gserviceaccount.com does not exist.

候補として挙がる次の3つは、いずれも存在しませんでした。

billing-budgets@system.gserviceaccount.com
billing-budgets-pubsub@system.gserviceaccount.com
cloud-billing-budgets@system.gserviceaccount.com

正しいプリンシパルは billing-budget-alert@system.gserviceaccount.com です。ドメイン制限共有の除外設定のページに、Pub/Sub で予算アラートを受け取る際のプリンシパルとして載っています。

手で付けなくても通知は届きました。Pub/Sub 連携を設定する側に pubsub.topics.setIamPolicy があれば、Publisher ロールは自動で付きます。

公式ドキュメントにも具体的なアドレスの記載はありません。「Pub/Sub Publisherロールを付与する権限が必要」とだけ書かれています。

そこでバインディングを一切付けずに作成しました。結果は次のとおりです。

通知は届いた
#

Terraform が作るのはリソースまでで、権限の付与と配信は Budget API 側が動かします。

sequenceDiagram
    participant TF as Terraform
    participant BG as 予算 / Budget API
    participant T as Pub/Sub トピック
    participant SUB as サブスクリプション

    TF->>T: トピックとサブスクリプションを作る
    TF->>BG: 予算を作る(all_updates_rule)
    BG->>T: roles/pubsub.publisher を自分に付与
    Note over BG,T: IAM バインディングは1つも書いていない
    BG->>T: 最初の通知(今回は作成から約7分後)
    T->>SUB: pull で1件
    Note over BG,SUB: 以降も1日に複数回。at-least-once で重複も順序入れ替わりもある

作成してから、サブスクリプションを1分おきに見ました。

02:30:32  予算作成
02:31:13  受信: 0件
02:32:34  受信: 0件
02:33:54  受信: 0件
02:35:15  受信: 0件
02:36:36  受信: 0件
02:37:39  受信: 1件

約7分で届きました。 Terraform では IAM バインディングを1つも書いていません。誰に権限が付いたのかを見ます。

gcloud pubsub topics get-iam-policy tf-adv-budget-notifications --format=json
{
  "bindings": [
    {
      "members": [
        "serviceAccount:billing-budget-alert@system.gserviceaccount.com"
      ],
      "role": "roles/pubsub.publisher"
    }
  ]
}

Budget API が自分で付けています。設定する側に pubsub.topics.setIamPolicy があれば足りました。

ただし組織ポリシー次第です。ドメイン制限共有を有効にしていると、billing-budget-alert@system.gserviceaccount.com の付与自体が拒否されます。その場合は除外設定が要ります。

Pub/Sub 通知が送られるのは、閾値を超えた瞬間ではありません。現在の予算の状態が1日に複数回送られます。

公式には初回まで数時間かかることがあるとされています。今回7分で届いたのは早いほうです。

配信は at-least-once です。同じ内容が複数回届くことも、順序が入れ替わることもあります。 通知を受けて処理を書くなら、何度実行しても同じ結果になるようにします。

通知の中身
#

gcloud pubsub subscriptions pull tf-adv-budget-notifications-sub \
  --auto-ack --limit=3 --format=json

data は Base64 でエンコードされた UTF-8 の JSON です。以下は jq で属性と本文を取り出して整形したもので、pull の生の出力そのままではありません。

属性です。

billingAccountId: BILLING_ACCOUNT_ID
budgetId: 3198661d-c1c0-4446-862b-a89a93f6754f
schemaVersion: 1.0

本文です。

{
  "budgetDisplayName": "tf-adv-budget-pubsub",
  "alertThresholdExceeded": 1.0,
  "costAmount": 110.03,
  "costIntervalStart": "2026-09-01T07:00:00Z",
  "budgetAmount": 1.0,
  "budgetAmountType": "SPECIFIED_AMOUNT",
  "currencyCode": "JPY",
  "forecastThresholdExceeded": 1.0
}

attributesにbudgetIdがあるので、複数の予算を1つのトピックに集約しても振り分けられます。

dataには累積コスト(costAmount)と予算額(budgetAmount)の両方が入ります。超過分を計算するのに追加のAPI呼び出しは要りません。

costAmount の定義は「amount of costs accrued」で、確定した請求額ではありません。使用量からコストへの反映には遅れがあります。

上限を超えてもリソースは止まらなかった
#

予算額1円に対して costAmount は110.03円。alertThresholdExceeded: 1.0が立っています。

この110.03円は、この検証で使った額ではありません。 costIntervalStart が 2026-09-01 なので、同じプロジェクトの当月の累計です。予算の対象をプロジェクト全体にしているため、他の検証で動かしていた費用が入っています。

gcloud pubsub topics list --format="value(name.basename())"
tf-adv-budget-notifications

トピックは一覧に残っていました。ただしこれは一覧に出るかどうかを見ただけで、送受信が続くかは確かめていません。

この検証で作ったのは Pub/Sub のトピックとサブスクリプションだけです。言えるのは「超過しても、このトピックが自動で消えることはなかった」ところまでです。 停止されたかどうかも、その後の課金も測っていません。

通知だけの予算が spending cap ではないことは公式の仕様です。実測はそれと矛盾しませんでした。

止めたいなら、この通知を受けて停止する処理を自分で書きます。Cloud Functions や Cloud Run へ push 配信し、そこでインスタンスを止める形です。

請求を無効化する方法も公式に案内されていますが、サービスが止まりデータに影響が出ることがあります。 副作用の大きい選択肢です。

「予算を設定したから使いすぎない」と考えていると、請求書を見て驚くことになります。

CURRENT_SPEND と FORECASTED_SPEND(仕様の整理)
#

ここは実測ではなく、設定した閾値ルールの整理です。どちらがいつ発火したかまでは比較していません。

閾値ルールは2種類の基準を持てます。

thresholdRules:
- spendBasis: CURRENT_SPEND
  thresholdPercent: 0.5
- spendBasis: CURRENT_SPEND
  thresholdPercent: 1.0
- spendBasis: FORECASTED_SPEND
  thresholdPercent: 1.0

CURRENT_SPENDは既に使った金額、FORECASTED_SPENDは期間終了時点の予測です。

前者は「もう使った」、後者は「このままだと超える」を知らせます。答える問いが違うので、必要に応じて併用します。

Spend Cap Budget は、確かめた範囲では Terraform から作れなかった
#

冒頭で触れた Spend Cap Budget は、この記事の方法では作れません。 3つの層すべてに無いためです。

gcloud にオプションがありません。

gcloud billing budgets create --help | grep -icE "spend.?cap|enforce"
0

alpha と beta でも 0 でした。create の引数一覧には --billing-account、--budget-amount、--filter-projects、--notifications-rule-pubsub-topic などが並びますが、上限を強制する引数はありません。

Budget API v1 にもフィールドがありません。リソースの項目は name / displayName / budgetFilter / amount / thresholdRules / notificationsRule / etag / ownershipScope で、spendCap にあたるものは含まれません。

google_billing_budget の引数にもありません。

amount / billing_account / display_name / budget_filter
threshold_rules / all_updates_rule / ownership_scope / deletion_policy

公式の手順も Console だけを案内しています。確かめたのは gcloud 562.0.0、Budget API v1、google provider 7.46.0 です。どれにも手段がありませんでした。

公開 API に出れば provider が追える性質のものなので、将来もできないという話ではありません。

制約も強めです。

項目 制限
スコープ 単一プロジェクト × 単一サービスのみ
対象サービス Gemini API / Gemini Enterprise Agent Platform / Cloud Run / Cloud Run functions
期間 Monthly 固定(毎月1日開始)
対象外 Folder、Organization、ラベル、複数プロジェクト、複数サービス

停止も即時ではありません。

the enforcement of spend caps aren’t instant and any cost overages are billed as normal

止まるのは新規の利用だけです。公式は3つを分けて書いています。

  • 処理中のリクエストは完了まで進み、そのぶん課金される
  • Compute や Storage のような永続リソースの固定費は止まらない
  • コスト反映の遅れによる超過分は通常どおり課金される

解除は Console で「Lift spend cap」を選ぶ必要があり、サービスの復帰に最大1時間かかります。

IaC で完結させたいなら、当面は通知を受けて自分で止める側に寄せることになります。

予算の操作は監査ログで追えなかった
#

gcloud logging read 'protoPayload.serviceName="billingbudgets.googleapis.com"' \
  --limit=5 --freshness=8h
(0件)

同じ時間範囲では、他サービスの監査ログを確認できました。ログ機能そのものは動いています。

billingbudgets   0件
pubsub           10件
serviceusage     8件

ここで「予算の操作は監査ログに残らない」と書きたくなりますが、それは言い過ぎです。

予算は請求アカウント配下のリソースなので、監査ログも請求アカウントのスコープに出ている可能性があります。今回はそのスコープで検索して0件でした。

ただし「権限はあったが0件だった」とまでは確かめていません。 実行者へのロール付与状況を見ていないためです。

gcloud billing accounts get-iam-policy BILLING_ACCOUNT_ID --format="value(bindings.role)"
roles/billing.admin
roles/billing.costsManager
roles/billing.user

gcloud logging read --billing-account=...は何も返しませんでした。ただし、これを権限不足とは判断できません。

roles/billing.adminには logging.logEntries.list と logging.privateLogEntries.list が含まれています。

$ gcloud iam roles describe roles/billing.admin --format='value(includedPermissions)' \
    | tr ';' '\n' | grep logging
logging.logEntries.list
logging.logServiceIndexes.list
logging.logServices.list
logging.logs.list
logging.privateLogEntries.list

また、get-iam-policy が出すのはロール名だけで、実行者への付与状況までは分かりません。ロールの権限は確かめましたが、0件だった原因はそこまでで追えていません。

そもそも Budget API は監査ログを出さない可能性が高いです。監査ログに対応するサービスの一覧に cloudbilling.googleapis.com は載っていますが、Budget API の billingbudgets.googleapis.com は載っていません。Cloud Billing の監査ログにも、予算の作成・更新・削除は対象メソッドとして挙がっていません。

2026年9月の時点では、予算の操作を Cloud Audit Logs で追えるとは確認できませんでした。 権限を足しても、そもそも出ていないなら見つかりません。

後片付け
#

terraform destroy
gcloud billing budgets list --billing-account=BILLING_ACCOUNT_ID --filter="displayName:tf-adv"
(0件)

terraform state listが0件でも、請求先アカウント側で予算が消えたことを確かめます。 予算は請求アカウント配下にあるためです。

まとめ
#

  • 予算はプロジェクトに属さない。 請求先アカウント配下にあり、プロジェクトを消しても残る
  • User ADC ではproviderにuser_project_overrideとbilling_projectが要る。API リクエストのクォータプロジェクトを指定する
  • budget_filter.projectsはproject 番号を要求する
  • Publisher ロールは Budget API が自分で付ける。 トピックの IAM Policy を見ると billing-budget-alert@system.gserviceaccount.com が入っている。よく挙がる3つのアドレスは、2026年9月時点では受け付けられなかった
  • 通知は届く。 今回は作成から約7分。ただし公式には初回まで数時間かかることがある。閾値超過の瞬間ではなく現在の状態が1日に複数回送られ、配信は at-least-once
  • 通知には累積コストと予算額の両方が入る。attributesのbudgetIdで複数予算を振り分けられる
  • 通知だけの予算は課金を止めない(公式仕様)。上限1円に対し110.03円でもトピックは一覧に残った。実測で言えるのは「消えなかった」ところまで
  • Spend Cap Budget は、確かめた範囲では Terraform から作れなかった。 gcloud 562.0.0・Budget API v1・provider 7.46.0 のどれにも該当する引数が無く、公式の手順も Console だけ。公開 API に出れば provider が追える
  • 予算の操作は監査ログで追えなかった。 billingbudgets.googleapis.com は監査ログ対応サービスの一覧に無く、2026年9月の時点では追跡できると確認できない

参考資料
#

Terraform-GoogleCloud - この記事は連載の一部です
パート 39: この記事

関連記事

terraform applyが成功してもVMの中は設定されていなかった話
4 分
Terraform GoogleCloud Ansible ComputeEngine CloudLogging
標準出力に1行JSONを書くだけなら、GKEではFluentdサイドカーが要らなかった
2 分
Terraform GoogleCloud GKE CloudLogging Fluentd
GKEのdefault_compute_class_enabledを有効にしても何も起きなかったので調べた
3 分
Terraform GoogleCloud GKE
GKEノードプールのSURGEアップグレードをBlue/Greenと同じ条件で測って比べてみた
3 分
Terraform GoogleCloud GKE
GKEノードプールをBlue/Greenでアップグレードして、soak期間中にロールバックしてみた
5 分
Terraform GoogleCloud GKE CloudKMS
GKEノードのブートディスクをCMEKで暗号化して鍵をローテーションしてみた
6 分
Terraform GoogleCloud GKE CloudKMS CMEK