【How-to Guides】GCP FinOps — 使用 Google Cloud Billing API 自動監控成本

Kellen
14 min readApr 21, 2024

--

使用 Google Cloud Billing API 自動監控成本

在雲端計算中,成本監控是一個關鍵的議題。Google Cloud 提供了 Billing API,可讓自動化監控您的雲端計算成本。本篇文章中,我們將更細一步的介紹如何使用 Billing API 將計費資訊推送到 Pub/Sub,然後使用 Google Cloud Functions 進行處理並將警示通知發送至 Microsoft Teams。

關聯文章
【How-to Guides】GCP Billing — 透過 Push Mail 或以 Slack 進行 billing alert notification

FAQ

01. 除 📧Push Mail 預算提醒通知外,有無其他方式可以達成?

在設定「預算與快訊」選單中,還可以鉤選「將 Pub/Sub 主題連結到這筆預算」

選取專案和 Pub/Sub 主題。凡是可查看這筆預算的使用者,皆能查看專案 ID 和主題名稱。如果某個 Pub/Sub 主題屬於已啟用網域限定共用 的機構,則可能無法新增。

02. 實際將 Billing 與 Pub/Sub Topic 連結,具體 GCP 作了什麼

  • 在「預算與快訊」選單設定完後,GCP 就會持續將帳務的資訊,頻率約 50 mins 發送至你剛指定的 Topic 之中,送出的 message 內容如下
  • @type: 這個鍵對應值,說明了這是一個 Pub/Sub 的 message
  • attributes: 這個鍵對應的值是一個字典,其中包含了消息的屬性資訊
  • data: 這個鍵對應的值是一個字串,是一段 Base64 編碼的字串

03. Message 中的資訊 attributes 與 data 說明

  • attributes: 包含了 'billingAccountId', 'budgetId', 'schemaVersion' 等屬性的資料,可先將 Base64 編碼的字符串 data 解碼為 UTF-8 字符串
notification_data = base64.b64decode(data).decode("utf-8", errors="ignore")
  • base64.b64decode(data): 使用 Base64 解碼函數 b64decode() 將 Base64 編碼的字符串 data 解碼成二進制數據
  • .decode("utf-8", errors="ignore"): 使用 decode() 方法將解碼後的二進制數據轉換為 UTF-8 字符串
  • errors="ignore" : 參數表如果出現解碼錯誤,忽略錯誤部分,而不引發異常

呈現狀況如下

budgetDisplayName 則是你在「預算與快訊」定義預算監控的名稱
https://cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications

04. Message 中的 data 對於設定告警的門檻值運作說明

alertThresholdExceeded 的值的出現與否,會依據你目前費用有無達到你設定的門檻值,若有觸發到你指定的告警門檻時,才會再自動帶出 alertThresholdExceeded

若還未達到你設定的門檻值 alertThresholdExceeded 的資訊就不會出現

05. 怎麼設計處理超出預算告警的邏輯

  1. 設計 Google Cloud Functions 去處理 message,將 Google Cloud Functions 與 Pub/Sub 整合起來,用於處理預算告警事件
  2. 因 Google Cloud Functions 函數可以設定為 Pub/Sub 作為觸發器,它可以接收到一個事件(event)參數和一個上下文(context)參數
  3. 在使用 Pub/Sub 作為觸發器時,事件參數(通常稱為 data 參數)將包含 Pub/Sub 收到的消息的內容,這通常是以 JSON 格式的字串形式提供的

06. 組織內有上數十、上百專案時,該如何達成

  • 【共用設計】可在 admin 的專案先設定好共用的 Topic,各專案在設定 billing alert 在選擇即可。例如,先在 infra 的專案設定 Billing alert 作業後並將 PubSub topic 設定為 admin-finops-channel 後,其他專案在進行 billing alert 時,指向此 Topic 使用(其他專案使用時進行切換即可選擇)

1. 簡易步驟說明

  1. 👣 步驟一:設置 Billing Alert 作業
  2. 👣步驟二:建立 Pub/Sub 主題
  3. 👣步驟三:設置監聽器和通知處理(GCF)
  4. 👣步驟四:發送通知至 Microsoft Teams or Slack 等

2. Google Cloud Functions 實作處理

說明

預計將作一個「全組織」項下的所有專案的 billing alert 通知機制,好處是只要設定一個 Google Cloud Function 來處理全專案的帳務告警處理。而帳務的門檻值則交由使用專案的 Owner 來處理即可!一旦設定好 Billing alert 後,將統一 push 帳務資訊至管理員的 Teams 管理 Channel 作統一監控。

而發送的通訊軟體選定 Teams 的 Channel 並以 Incoming Webhook 處理,並將 url 儲存在 Secret Manager 之中,設計成呼叫指定函數與 IAM 權限控管後,即可拿取 access_secret_version

資料整理

  • 以 data[“data”] 為主體,因裡面富含了 Billing Alert, 最新帳務費用等,並逐一進行加工
  • 移除 data[“data”] 不需使用的欄位、對時間欄位格式化成 202404(目的是簡化作為帳單月份)
  • 【重要】本次實作會將全組織內的數十個專案的 billing alert 噴至同一個 Topic,綜觀 message 由於沒有相關專案的資訊會夾帶於 message 之中,因此在設定 alert 時,要讓名稱作為可辦識來不同專案之用途
billing alert 名稱取名為 project name 有其特定用法
  • 【選項】視需求緣故,可將 attributes 的 billingAccoudId 抽出來,這個可以併回到 data[“data”] 的主體資訊中

通知函數 notify_teams

我們已在 Google Cloud Function 中實現處理邏輯,也可以使用 Google Cloud Secret Manager 來存儲 Microsoft Teams 的 Webhook URL,然後在函數中訪問它。最後,將 Pub/Sub 推播過來的訊息轉化成帳務管理的精簡資訊 ,並使用 Python 中的 requests package 傳送 POST 請求將通知發送至 Microsoft Teams。

import json
import base64
import requests
import logging
import os
import pytz
from datetime import datetime, time
from google.cloud import secretmanager

# 初始化日誌記錄器
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logging.info('start FinOps billing alert to teams')

def is_midday():
tz = pytz.timezone('Asia/Taipei')
now = datetime.now(tz).time()
print(f"Current time: {now}") # 加入此行來檢查當前時間
return time(18, 0) <= now <= time(18, 59, 59)

def access_secret_version(project_id, secret_id):
# 創建一個 Secret Manager 客戶端
client = secretmanager.SecretManagerServiceClient()
# 構建密鑰的名稱
secret_name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
# 提取密鑰版本
response = client.access_secret_version(request={"name": secret_name})
# 提取密鑰資訊
payload = response.payload.data.decode("UTF-8")

return payload

# Teams Webhook URL
WEBHOOK_URL = access_secret_version("proj-XXXX-infra", "swlab-admin-webhook-url")

def notify_teams(data, context):
if not is_midday():
logging.info("Not in the specified time range. Skipping message.")
return
logging.info("In the specified time range. Start Notification")

# data 為 pubsub_message
logging.info(data)

try:
notification_attr_dict = data["attributes"]
billing_account_id = notification_attr_dict['billingAccountId']
except KeyError:
notification_attr = "No attributes passed in"

try:
notification_data_str = base64.b64decode(data["data"]).decode("utf-8", errors="ignore")
notification_data = json.loads(notification_data_str)
except KeyError:
notification_data = "No data passed in"

# ---- Output Beautiful ----
# 1. Remove budgetAmountType field
if "budgetAmountType" in notification_data:
del notification_data["budgetAmountType"]

# 2. Modify alertThresholdExceeded to percentage format if it exists 0.85 -> 85%
if "alertThresholdExceeded" in notification_data:
alert_threshold = notification_data.get("alertThresholdExceeded")
if alert_threshold is not None:
notification_data["alertThresholdExceeded"] = f"{int(float(alert_threshold) * 100)}%"

# 3. Convert costIntervalStart "2023-10-01T07:00:00Z" to YYYYMM format "202310"
cost_interval_start = notification_data.get("costIntervalStart", "")

try:
# Parse the date string and format it
cost_interval_start = datetime.strptime(cost_interval_start, "%Y-%m-%dT%H:%M:%SZ").strftime("%Y%m")
except ValueError:
logging.error("Error parsing costIntervalStart")
pass # Ignore if the date string is not in the expected format

notification_data["costIntervalStart"] = cost_interval_start

# 4. 新增 billingAccountId 資訊到 notification_data
notification_data['billingAccountId'] = billing_account_id

# Final 資訊彙整至此
budget_notification_text = f"{notification_data}"
logging.info(budget_notification_text)

payload = {
"text": budget_notification_text
}

headers = {
"Content-Type": "application/json"
}

try:
response = requests.post(WEBHOOK_URL, json=payload, headers=headers)
response.raise_for_status()
print("Message sent to Teams successfully")
except requests.exceptions.RequestException as e:
print(f"Error posting to Teams: {e}")

最後發送到 Teams 指定的 Channel 再搭配機器人或是通知軟體的 @tag 功能,扣除掉工人智慧外,這樣的設計方式,是可以最快攔截到異常帳務的手段。此外,Teams 為 Azure 的產品之一,可以使用 Power Automate 去監聽 Channel 的關鍵字產生,預算管理員可進一步採取其他措施,例如有超逾 100% 轉開工單請 Owner 需進行說明;或是超逾 95% 直接在 Power Automate 的 Channel 進行告警的提醒等設計。

3. 總結

使用 Google Cloud Billing API、Pub/Sub 和 Cloud Functions 可以達成自動化監控雲端計算成本並發送通知。這提供了即時的成本監控和管理能力,幫助優化雲端資源使用並降低成本。

Reference

  • None

--

--

Kellen

Backend(Python)/K8s and Container eco-system/Technical&Product Manager/host Developer Experience/早期投入資料創新與 ETL 工作,近期堆疊 Cloud☁️ 解決方案並記錄實作與一些雲端概念💡