【How-to Guides】GCP Billing — 超出預算,即刻進行資源縮減或帳單移除

How to build BudgetSavior Cloud function to cap your Billing budget.

Kellen
17 min readOct 30, 2023
Build your BudgetSavior Cloud Function

背景說明

雲端預算控管應該是大家都在持續努力的領域,本篇將使用 GCP Billing 為出發點,將 Billing 訊息可以使用 PubSub 傳遞給 BudgetSavior Cloud Function。一旦超出預算比例,BudgetSavior 將立即採取相應措施,例如停止特定的 GCP 資源,如 Compute Engine 實例,或是移除帳單結算功能,希望讓不同場景下的雲端可以合理使用並不會超出約定的預算範圍,讓 IT 成本管理變得更輕鬆!

科普 — Compute Engine 狀態與收費資訊

因為要對 Compute Engine 作資源縮減的動作,必須先了解 GCP 在對於停止(Stop)、暫停(Suspending)、刪除(Delete)、重啟(Start)生命週期的狀態及收費標準。

關應用程式呼叫 Compute Engine API 作法:

  1. 停止和啟動虛擬機,並需要虛擬機器的 compute.instances.stop 權限
  • API Client 引入目前看到 GCP 文件使用到有兩種不同套件用法 google.cloud or googleapiclient,都可以運作
from google.cloud import compute_v1

instance_client = compute_v1.InstancesClient()

# -------------------------------------------------
from googleapiclient import discovery

compute = discovery.build("compute", "v1", cache_discovery=False,)
instances = compute.instances()
instances.stop(project=project_id, zone=zone, instance=name).execute()
https://cloud.google.com/billing/docs/how-to/notify

2. 刪除虛擬機器實例

在支出超過預算時直接砍掉正在運行的虛擬機器實例 ٩(๑´0`๑)۶ 這有點狠,但管理員或預算維護者喜歡!

💡 Example

有了 Compute Engine 的狀態與收費認知後,接續來實作一個當達到預算上限的門檻,啟動 (cap)Billing 的作法,設計一支 BudgetSavior 的 Cloud Function,若超過預算的約定,直接將 Compute Engine 進行停止。

主程式介紹

  • 架構設計

前一篇我們已經成功將 Billing 推播至 Slack
🔗【How-to Guides】GCP Billing — 透過 Push Mail 或以 Slack 進行 billing alert notification

  • 前置作業
    導入模組和設定專案資訊:首先,導入所需的模組,包括base64、json、os,以及從googleapiclient中導入discovery。然後,從環境變數中取得Google Cloud專案ID(PROJECT_ID)並設定虛擬機器實例所在的區域(ZONE)
  • limit_use Function
    這是主函數,它接受一個 Pub/Sub 訊息以進行觸發(以 data 和 context 作為參數),解析該訊息,並根據專案的當前支出與預算的來比較是否終止運行中的虛擬機實例(依照您的場景可自行調整)。Pub/Sub 訊息解析(base64 解碼),然後將其解析為 JSON 格式,提取出當前的費用(costAmount)和預算金額(budgetAmount)。
  • __list_running_instance Function
    此函數用於啟動指定專案和區域中執行中的虛擬機器實例的名稱。透過呼叫 Compute Engine API 方法,取得虛擬機器實例列表,並過濾出狀態為「RUNNING」的實例名稱。然後 instances.list 將實例名稱傳回。
  • __stop_instance Function
    該函數用於停止虛擬機器實例的作業,並由 __list_running_instance 提供要停止的名稱列表,然後調用Compute Engine API 方法來停止虛擬機器實例。每次停止成功後,函數會列印一則訊息,表示虛擬機器實例已成功失效。
  • 資料處理與轉換細節
    PubSub 會引入 data 格式可參考以下,然後因為有些被 Base64 加密,記得要反解回來,然後整理出所需要的欄位,或是制定自己的規則(例如截至本日可花費的門檻值、已使用比率、使用比率是否已達超支),規格這邊可以參考 GCP 有一些規格參考資訊
# PubSub sample data
{
"budgetDisplayName": "billing-notification",
"alertThresholdExceeded": 0.85,
"costAmount": 218.98,
"costIntervalStart": "2023-10-01T07:00:00Z",
"budgetAmount": 250.0,
"budgetAmountType": "SPECIFIED_AMOUNT",
"currencyCode": "USD"
}

#
def stop_billing(data, context):
# 對 PubSub 傳出的 data 中的 Base64 編碼數據進行解碼,然後使用 decode()方法
# 將解碼後的字節轉換為 UTF-8 編碼的字符串。解碼後的數據被存儲在變量 pubsub_data 中
pubsub_data = base64.b64decode(data["data"]).decode("utf-8")

# 用 json.loads() 函數將解碼後的 JSON 字符串 pubsub_data 轉換為Python字典。
# 轉換後得到的字典被存儲在變量 pubsub_json 中
pubsub_json = json.loads(pubsub_data)

# 這行代碼從 pubsub_json 字典中提取 “costAmount” 鍵的值
# 並將其賦值給變量 cost_amount
cost_amount = pubsub_json["costAmount"]
budget_amount = pubsub_json["budgetAmount"]

切入主程式,目的是在雲端運算環境下監控雲端支出,並花費超過預算時間中斷運行中的虛擬機器實例,以控制成本。

import base64
import json
import os

from googleapiclient import discovery

PROJECT_ID = os.getenv("GCP_PROJECT")
PROJECT_NAME = f"projects/{PROJECT_ID}"
ZONE = "us-west1-b"

def limit_use(data, context):
pubsub_data = base64.b64decode(data["data"]).decode("utf-8")
pubsub_json = json.loads(pubsub_data)
cost_amount = pubsub_json["costAmount"]
budget_amount = pubsub_json["budgetAmount"]
if cost_amount <= budget_amount:
print(f"No action necessary. (Current cost: {cost_amount})")
return

compute = discovery.build(
"compute",
"v1",
cache_discovery=False,
)
instances = compute.instances()

instance_names = __list_running_instances(PROJECT_ID, ZONE, instances)
__stop_instances(PROJECT_ID, ZONE, instance_names, instances)

def __list_running_instances(project_id, zone, instances):
"""
@param {string} project_id ID of project that contains instances to stop
@param {string} zone Zone that contains instances to stop
@return {Promise} Array of names of running instances
"""
res = instances.list(project=project_id, zone=zone).execute()

if "items" not in res:
return []

items = res["items"]
running_names = [i["name"] for i in items if i["status"] == "RUNNING"]
return running_names

def __stop_instances(project_id, zone, instance_names, instances):
"""
@param {string} project_id ID of project that contains instances to stop
@param {string} zone Zone that contains instances to stop
@param {Array} instance_names Names of instance to stop
@return {Promise} Response from stopping instances
"""
if not len(instance_names):
print("No running instances were found.")
return

for name in instance_names:
instances.stop(project=project_id, zone=zone, instance=name).execute()
print(f"Instance stopped successfully: {name}")

完成上架及可以看到有訂閱 Billing API 的 Topics

運行結果,確實被關機。

💡 Example

實作一個當達到預算上限的門檻,啟動透過限制(停用)結算功能來停止使用,這邊直接將 Billing Account 結算功能移除!
註:停用專案結算功能將導致該專案中的所有 Google Cloud 服務(包括免費層級服務)終止

資源未正常關停,刪除後可能無法恢復。如果停用 Cloud Billing,則無法正常復原。
您可以
重新啟用Cloud Billing,但無法保證服務能夠恢復,而且需要進行手動設定,作法上滿暴力要選合比較合適的場景

主程式講解

  1. stop_billing Function:這是主函數,它接受一個 Pub/Sub 訊息(以 data和 context 作為參數)後被觸發,可以根據專案的當前支出與預算的比較來決定是否停止結算功能。並會呼叫__is_billing_enabled 函數來決定的後續的動作,如果 Billing Account 是啟用狀態,將執行專案終止 Billing Account 的步驟;如果 Billing Account 已被取消,就簡單列印訊息傳回,告知 Billing Account 已被取消,也不用擔心帳單超支問題了。
  2. __is_billing_enabled Function:此函數用於檢查指定專案的 Billing Account 計費狀態。調用 Cloud Billing API 來獲取專案的計費資訊,然後檢查 billingEnabled 字段。如果 billingEnabled 為 True,表示計費已啟用;如果 billingEnabled 為 False 則表示已停用 Billing Account 結算功能。
  3. __disable_billing_for_project Function:此函數用於將此專案停用 Billing Account 計費結算功能,建立一個請求體(body),將billingAccountName 設定為空字串(作法是塞入空字串,就表示沒有帳單資訊喔!),然後呼叫 Cloud Billing API 的 updateBillingInfo 方法來更新專案的計費訊息,從而計費結算功能失效。
import base64
import json
import os

from googleapiclient import discovery

PROJECT_ID = os.getenv("GCP_PROJECT")
PROJECT_NAME = f"projects/{PROJECT_ID}"
def stop_billing(data, context):
pubsub_data = base64.b64decode(data["data"]).decode("utf-8")
pubsub_json = json.loads(pubsub_data)
cost_amount = pubsub_json["costAmount"]
budget_amount = pubsub_json["budgetAmount"]
if cost_amount <= budget_amount:
print(f"No action necessary. (Current cost: {cost_amount})")
return

if PROJECT_ID is None:
print("No project specified with environment variable")
return

billing = discovery.build(
"cloudbilling",
"v1",
cache_discovery=False,
)

projects = billing.projects()

billing_enabled = __is_billing_enabled(PROJECT_NAME, projects)

if billing_enabled:
__disable_billing_for_project(PROJECT_NAME, projects)
else:
print("Billing already disabled")

def __is_billing_enabled(project_name, projects):
"""
Determine whether billing is enabled for a project
@param {string} project_name Name of project to check if billing is enabled
@return {bool} Whether project has billing enabled or not
"""
try:
res = projects.getBillingInfo(name=project_name).execute()
return res["billingEnabled"]
except KeyError:
# If billingEnabled isn't part of the return, billing is not enabled
return False
except Exception:
print(
"Unable to determine if billing is enabled on specified project, assuming billing is enabled"
)
return True

def __disable_billing_for_project(project_name, projects):
"""
Disable billing for a project by removing its billing account
@param {string} project_name Name of project disable billing on
"""
body = {"billingAccountName": ""} # Disable billing
try:
res = projects.updateBillingInfo(name=project_name, body=body).execute()
print(f"Billing disabled: {json.dumps(res)}")
except Exception:
print("Failed to disable billing, possibly check permissions")

場景使用探討

如 NBA 薪資上限(NBA Salary Cap)一樣,每支 NBA 球隊在每個賽季中可以用來支付球員薪資的最高總額。這一規定旨在確保聯盟中的競爭公平性,防止一些球隊因過高的薪資支出而獲得不公平的競爭優勢。組織內部成本也一樣要控制,避免不必要的開銷,因為在某些情況下,可能購買外部資源會比在內部開發更經濟實惠(Solution, Model 購置等…),方向上也可以有一些作法進行多邊在預算的協商來取得平衡:

  • 沙盒或實驗環境:與預算管理員共同約定超逾預算的作法(或是類比是合約),可以強制性移除 Billing 或讓特定資源直接降規或停止,開發團隊也要被賦予在一定預算內進行作業,並確保公司保持財務穩健。
  • 營運環境:因涉及顧客或營運服務,要非常仔細,例如:那項作業最花成本,降規或降速對服務會有影響嗎?你的利害關係人可否接受等多方共同協商的過程,無法直接套用資源全數移除的作法!但可以藉由協調過程開始踏出第一步。

以上目的都是防止在短時間爆發大量的資源需求,確保公司能夠長期持續地提供高品質的服務和支援。

--

--

Kellen

Backend(Python)/K8s and Container eco-system/Technical&Product Manager/host Developer Experience/早期投入資料創新與 ETL 工作,近期研究 GCP/Azure/AWS 相關的解決方案的 implementation