Barbara Shaurette

Python Developer and Educator

Monitoring GCP Costs with Pub/Sub and Python: Part 1

2025-05-16


One of my biggest complaints about working with Google Cloud is that the costs are often obfuscated or so difficult to calculate that they may as well be hidden. As a developer working in that platform, I found it easy to ignore costs and build whatever I wanted, especially when someone else was footing the bill. Now that I'm on SRE, I'm the person moniitoring those costs and helping teams figure out how to build more cost-efficiently.

One of the tools I use is a set of monthly budget alerts that write Pub/Sub notifications integrated with Slack. This post is part 1 of 2, where I'm going to talk about programatically creating the budget alerts that route messages to Pub/Sub. In part 2, I'll go over the Cloud Run function that ingests those messages and alerts to Slack.

Budget alerts are a Cloud Billing tool that let you monitor costs on a monthly or quarterly basis. You set a desired spending threshold for a project, and the monitor alerts you (usually by email) when your project has reached that threshold.

It's important to note that these alerts don't actually enforce any spending limits - if you have an expensive process running, that process will keep running, you'll just be notified when it goes over budget.

Setting up an alert through the console is simple and largely self-explanatory. Navigate to Billing > Budgets and alerts and select 'Create budget'.

You'll select a name, a time range, and the project you want to monitor.

Screen shot

Set your budget amount - that can be a specific dollar amount you want to stay under, or an amount relative to the previous month's spend.

Screen shot

You can adjust the threshold rules if you like, but some defaults are pre-defined for you. Select who you want to receive notifications, and if you're also planning to send the monitoring notifications to a Pub/Sub topic, select that.

Screen shot

You'll get a popup window that lets you create the new Pub/Sub topic in the same dialog.

Screen shot

But if you're part of an organization that has more than a few projects, creating these alerts through the console is going to get tedious.

If you use `gcloud` and are authenticated with the billing project you want to set these alerts in, you can do it from the command line.

For each project/alert, you'll need to create a JSON file that contains something like this:

{
  "displayName": "Keep my spending down",
  "budgetFilter": {
    "projects": [
      "projects/{PROJECT-ID}"
    ],
    "calendar_period": "QUARTER"
  },
  "amount": {
    "lastPeriodAmount": {}
  },
  "thresholdRules": {
    "thresholdPercent": 0.8,
    "spendBasis": "CURRENT_SPEND"
  }
}

Then you can post that JSON configuration to Google with a curl call that looks like this:

curl -X POST \
     -H "Authorization: Bearer $(gcloud auth print-access-token)" \
     -H "x-goog-user-project: {BILLING-PROJECT}" \
     -H "Content-Type: application/json; charset=utf-8" \
     -d @project.json \
     "https://billingbudgets.googleapis.com/v1/billingAccounts/{BILLING-ACCOUNT-ID}/budgets"

It's obviously pretty easy to generate these dynamically if you have a list of projects and you want to use the same threshold rules for all of them.

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "google-cloud-resource-manager==1.14.0",
# ]
# ///
import json
import os

from google.cloud.resourcemanager import ProjectsClient

all_projects = []

for project in ProjectsClient().search_projects():
    if not project.project_id.startswith("sys-"):
        all_projects.append({"create_time": str(project.create_time), "display_name": project.display_name, "name": project.name, "project_parent": project.parent, "project_id": project.project_id})

for p in all_projects:
    project_id = p["project_id"]
    filename = f"alert_{project_id}.json"
    display_name = f"Monthly Budget Alert - {project_id}"
    projects = f"projects/{project_id}"

    data = {
      "displayName": display_name,
      "budgetFilter": {
        "projects": [projects],
        "creditTypesTreatment": "INCLUDE_ALL_CREDITS",
        "calendar_period": "MONTH"
      },
      "amount": {
        "lastPeriodAmount": {}
      },
      "thresholdRules": [
        {"thresholdPercent": 0.5, "spendBasis": "CURRENT_SPEND"},
        {"thresholdPercent": 0.9, "spendBasis": "CURRENT_SPEND"},
        {"thresholdPercent": 1, "spendBasis": "CURRENT_SPEND"}
      ],
      "notificationsRule": {
        "pubsubTopic": "projects/my-billing-project/topics/budget-alerts-slack",
        "schemaVersion": "1.0",
        "enableProjectLevelRecipients": "true"
      }
    }

    with open(filename, "w") as f:
        json.dump(data, f)

    call = f"curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "x-goog-user-project: my-billing-project" -H "Content-Type: application/json; charset=utf-8" -d @{filename} "https://billingbudgets.googleapis.com/v1/billingAccounts/my-billing-account-id/budgets""
    print(call, "\n")

# run with:
# uv run create_budget_alerts.py

A few notes about what this code is doing:

This scripts generates a list of projects, creates a JSON file for each one, writes the file to a local directory, and generates a corresponding curl call that you can use to post the JSON budget alert configuration to Google. For more information about your configuration options, check out this page.

If you're authenticated with `gcloud` locally, `ProjectsClient().search_projects()` will generate a list of all of the GCP projects you have access to within your organization. You just need to filter out some of the important metadata and add it to the `all_projects` list.

In data['notificationsRule']['pubsubTopic'], and in the curl call at the bottom, there's a reference to `my-billing-project`. If your organization is maintaining multiple projects, Google recommends creating a separate project dedicated just to billing management. You don't have to do this, but I find the budget alerts a lot easier to manage that way. You can find more information about this "FinOps" recommendation here.

If you go this route, you'll end up with a batch of local JSON files and a list of curl calls that you could run individually. Of course, there are other ways to do this. You could run the curl calls from within the script, using `subprocess`. You could also just use the `requests` library instead of curl - you can get the Authorization token as a string by running `gcloud auth print-access-token` from the command line and plugging it into your script.

Regardless of how you do it, this will generate a batch of budget alerts which you'll be able to see in the console at this path:

https://console.cloud.google.com/billing/BILLING-ACCOUNT-ID/budgets?organizationId=ORGANIZATION-ID

Drop back by in a few weeks (I've got some travel coming up so I'll be a little delayed) for part 2, where I'll go over the components and code I use to route Pub/Sub messages into Slack, and how I keep those messages from creating a lot of noise (including some fake caching that prevents you having to set up a memory store).


Contact: barbara@mechanicalgirl.com
github linkedin mastodon bluesky pixelfed rss