[Tutorial] Setting up Budget Alert in GCP
This article is written in both Thai and English: https://mesodiar.com/tutorial-setting-up-budget-alert-in-gcp-english-ver-f134bf21da92
เคยไหมที่เปิด service บน cloud ไปแล้วเผลอลืมปิด โดนชาร์จไปแบบไม่รู้ตัว ?
เคยไหมที่ใช้ cloud ไปเยอะเท่าไร billing อัพเดทช้าไม่ทันใจ ไม่รู้ว่าตอนนี้ cost กินไปเท่าไรแล้ว ?
บทความนี้เราจะสอนวิธีเซ็ต budget alert flow เมื่อ cost ของการใช้งานสูงกว่า budget ที่กำหนดไว้ flow นี้ก็จะมี action บางอย่าง เช่น ส่ง notification มาหา 1 ครั้งต่อเดือน หรือทำ action บางอย่างแบบ automate ที่เราต้องการแบบมีเงื่อนไข
ปกติแล้ว ใน Cloud Billing ตัวมันเองจะสามารถส่ง noti หา Billing Admins อยู่แล้ว แต่ในเคสที่ต้องการ custom และมีเงื่อนไขการส่งที่มากขึ้น ตัว Cloud Billing เดี่ยวๆก็ไม่สามารถตอบโจทย์ได้
flow นี้จะใช้สำหรับ GCP โดยเฉพาะ เริ่มจาก Cloud Billing, Cloud Pub/Sub, Cloud Functions และมี Firestore
เคสนี้สามารถใช้ในเคสที่อยาก monitor cost ในโปรเจคตั้งแต่ 1 โปรเจคขึ้นไป หากมีโปรเจคเยอะๆ ก็จะช่วยทำให้ลดภาระการเช็ค cost แบบ manual ไปได้
ในตัวอย่าง scenarios นี้ของเราคือ
Scenario 1:
Given: มีการทำ query ข้อมูลที่ถี่ในเดือนนี้ เลยมีการใช้งาน BigQuery ที่สูง
When: cost เยอะกว่า 75% ของ budget ที่เรากำหนดไว้ต่อเดือน
Then: ระบบจะส่ง notification บอกเราว่าตอนนี้ cost เท่าไรและ reach 75% แล้ว
Scenario 2:
Given: มีการทำ query ข้อมูลที่ถี่ในเดือนนี้ เลยมีการใช้งาน BigQuery ที่สูง
When: cost เยอะกว่า 100% ของ Budget ที่เรากำหนดไว้ต่อเดือน
Then: ระบบจะส่ง notification บอกเราว่าตอนนี้มี cost เท่าไรและ reach 100% แล้ว + ปรับ pricing plan จาก on-demand เป็น BigQuery Editions
Prerequisites:
- Project ที่มีการใช้งาน BigQuery (หรือจะ service ไหนก็ได้) ถ้าอยากลองเล่นแบบไม่กลัว cost ลั่น แนะนำสมัคร GCP account ใหม่เพื่อเคลม 300$ credit ฟรี
- เราต้องมี role ที่ชื่อว่า
Billing Account Costs manager
หรือBilling Account Administrator
ซึ่งเป็นสิทธิที่สูงมากระดับ organization ถ้าอยู่บริษัทใหญ่ๆจะถือว่าใหญ่ทีเดียว หากอยากเทสง่ายๆแนะนำสมัคร account ใหม่ 555 - Budget ในใจ
- ช่องทางที่เราอยากรับ Notification เช่น Microsoft Teams
ถัดไป เราจะติดตั้งการใช้งานไล่ตามนี้ Cloud Billing > Cloud Pub/Sub > Cloud Firestore > Cloud Functions
Cloud Billing Budget & Alerts
- เมื่อเรามาที่ console.cloud.google.com ในช่อง search ให้เราใส่คำว่า ‘billing alert’ ก็จะมีคำว่า
Budgets & Alerts
ขึ้นมา
2. หากเรามีสิทธิ Billing Account Costs manager
หรือ Billing Account Administrator
เราจะสามารถเห็นหน้านี้ได้ และหน้านี้จะเป็น list ของ budget alert ที่เราสามารถเข้ามาเซ็ตได้
ให้กดที่ create budget เพื่อกดสร้าง
3. ทีนี้เราจะตั้งชื่อ alert นี้ว่า mils-project-bigquery-notification
โดยชื่อจะมาจากชื่อ GCP project และ BigQuery เพื่อสอดคล้องว่า alert นี้เป็นของ BigQuery ที่ใช้ในโปรเจคนี้
โดยจะเซ็ต time range เป็น Monthly ไว้ และเลือกเป็น service ตัว BigQuery ตัวเดียว
ซึ่งถ้าเราอยากให้เช็ค cost ทั้งโปรเจคก็เลือก service ทั้งหมดไปเลยได้นะ
คราวนี้อย่าลืมติ๊ก Discounts
กับ Promotions and Others
ออก เพราะว่าเราอยาก monitor cost ที่เกิดขึ้นจริงแต่แรก หากเรารวม Discounts มาด้วย เดี๋ยวมันจะไม่ส่ง noti มา
สังเกตเห็นได้ว่า หาก project นั้นมีการใช้งานอยู่แล้ว จะมีกราฟ Cost Trend แสดงมาด้านขวา
4. ใส่ Budget ที่ต้องการ
เราจะใส่ไป 300 Baht ถึงแม้ว่าการใช้งานจริงจะทะลุไปแล้วก็เถอะ5555
4. เลือก Threshold ที่ต้องการ
โดยเราจะเลือก Threshold ที่ 50%, 75%, 100% ซึ่งผลสุดท้ายแต่ละ Threshold จะมี action ที่แตกต่างกัน ซึ่งจะให้ Cloud Functions เป็นคน process logic ทีหลัง
จะเห็นได้ว่า Manage Notifications มี 3 choices ให้เลือก ซึ่ง 1 กับ 2 ต้องเลือกอย่างใดอย่างหนึ่ง หาก GCP project นี้เราใช้เป็น email ปัจจุบันเองก็จะไม่ต่างกันเท่าไร
แต่หากเราใช้ account ที่สร้างใหม่เพื่อเทส อาจจะต้องสร้าง Notification Channel เพิ่ม ก็สามารถเลือกข้อ 2 เป็นการ custom การส่งตาม email ที่ต้องการได้ สร้างเป็นอีเมลล์ต่อหา email ปัจจุบันที่เราใช้
ซึ่งมาถึงตรงนี้ หากคิดว่า flow ของเราต้องการแค่ส่ง email หรือใช้ webhook ตรงๆเลยก็ย่อมได้ แต่หากต้องการให้มีลูกเล่นในแต่ละเงื่อนไขของ Threshold เพิ่ม เราก็จะไปใช้ตัวเลือกที่ 3
ดังนั้นเราจะเลือกข้อ 3 ด้วย นั่นคือจะให้ส่งหา pub/sub ด้วยเพื่อจะเป็นสะพานไปส่งหา Cloud Functions อีกที ซึ่งลูกเล่นที่ว่านั้นจะอยู่ภายใน Cloud Functions นั่นเอง
Cloud Pub/Sub
ในตัวเลือกสุดท้าย เราจะสร้าง Pub/Sub topic จากหน้านี้แหล่ะ
โดยหลังจากเราจะเลือก create topics
มันจะมีหน้าต่างเด้งมา เราจะตั้งชื่อว่า mils-project-bigquery-budget-alert-topic
เราจะ enable message retention ด้วย เซ็ตไว้ที่ 31 วัน เพราะเราก็เก็บ message ไว้ตามจำเป็นพอ
คราวนี้กด save ก็เป็นอันเสร็จ
ถ้า cost ที่เกิดขึ้นจริงเยอะกว่า budget ก็จะเป็นสีส้มแบบนี้5555 แต่ถ้าไม่เกินก็จะเป็นสีเขียว
คราวนี้จะเราจะแวะไปดูที่ Cloud Pub/Sub
โดยเราจะเห็นว่า มี pub/sub topic ที่เราสร้างไว้จากหน้า Billing Alert ไว้แล้ว
เมื่อเรากดเข้ามา mils-project-bigquery-budget-alert-topic
จะเห็นว่า ยังไม่มี subscription อยู่ ดังนั้นเราจะต้องมากดสร้างตัว subcribe
กด create subscription
เราจะตั้งชื่อว่า mils-project-bigquery-budget-alert-sub
เพื่อสอดคล้องกับชื่อ topic
config ตามนี้ แล้วกด create
ซึ่งหากเราสร้าง subscribe เสร็จแล้ว เราจะเข้าไปดู message ที่เกิดขึ้นได้
subscriptionนั้น > tab ชื่อ messages > กด pull
แต่เนื่องจาก เราเพิ่งสร้าง subscription สดๆร้อนๆ มันเลยยังไม่มีของวิ่งมา
หากเราปล่อยไปสักพัก มันจะมี message วิ่งเข้ามาเรื่อยๆ ตัวอย่างแบบในรูป
Message Example in Pub/sub
Message ที่ subscribe ดึงมาจาก topic จะมีอยู่ 2 รูปแบบ
- Message ที่ cost ยังไม่ reach threshold เลย
{
"budgetDisplayName": "mils-project-bigquery-notification",
"costAmount": 0.0,
"costIntervalStart": "2023-07-01T07:00:00Z",
"budgetAmount": 1.0,
"budgetAmountType": "SPECIFIED_AMOUNT",
"currencyCode": "USD"
}
2. Message ที่ cost reach threshold แล้ว
{
"budgetDisplayName": "mils-project-bigquery-notification",
"alertThresholdExceeded": 1.0,
"costAmount": 120.0,
"costIntervalStart": "2023-07-01T07:00:00Z",
"budgetAmount": 100.0,
"budgetAmountType": "SPECIFIED_AMOUNT",
"currencyCode": "THB"
}
โดยความน่าปวดหัวนิดนึงของ Pub/sub คือ มันจะส่ง message มาเรื่อยๆ ไม่ว่า cost จะราคาเท่าไร แล้วหลังจากนี้ ถ้าเรา plug ตัว Cloud Functions เข้ามา มันก็จะไป trigger ตัว Cloud Functions ทุกครั้งที่ Pub/sub topic มี message เช่นกัน
เราเลยต้องเซ็ตให้ Cloud Functions รองรับการที่ message วิ่งมาซ้ำๆด้วย เนื่องจาก Cloud Functions ก็จะเป็นคนส่ง noti หาเรา เราคงไม่อยากให้ Noti ส่งหาเราพร่ำเพรื่อ
Cloud Functions
คราวนี้เราจะเซ็ตให้ Cloud Functions เราจะตั้งชื่อว่า budget-alert-notifications
ซึ่งเราต้องกด Add Trigger
เป็น Pub/Sub trigger ด้วย และเลือกชื่อจาก Pubsub topic ที่เราเซ็ตไว้
พอกด Next
ก็จะเป็นหน้าที่เซ็ตว่า Cloud Functions นี้จะทำอะไรบ้าง
เราจะให้ Cloud Functions เป็นคนรับค่าจาก message แล้วเช็คว่าใน message มี alertThresholdExceeded
หรือไม่
หากมี ก็จะแปลว่า cost ถึง threshold แล้ว ซึ่งเราก็จะแยก action ที่เกิดจาก threshole 75% และ 100%
โค้ดจึงออกมาหน้าตาคร่าวๆประมาณนี้ :D
# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def subscribe(cloud_event: CloudEvent) -> None:
# Print out the data from Pub/Sub, to prove that it worked
print(
"Hello, " + base64.b64decode(cloud_event.data["message"]["data"]).decode() + "!"
)
pubsub_data = base64.b64decode(cloud_event.data["message"]["data"]).decode("utf-8")
pubsub_json = json.loads(pubsub_data)
cost_amount = pubsub_json["costAmount"]
budget_amount = pubsub_json["budgetAmount"]
budget_display_name = pubsub_json["budgetDisplayName"]
alertThresholdExceeded = pubsub_json.get("alertThresholdExceeded", None)
if alertThresholdExceeded is None:
if cost_amount <= budget_amount:
print(f"No action necessary. (Current cost: {cost_amount})")
return
else:
content = """
Budget name '{}'<br>
Current cost is <b>${}</b>.<br>
Budget cost is <b>${}</b>.<br>
💸 💸 💸 💸
""".format(
budget_display_name, cost_amount, budget_amount
)
title = "💸 Cost reaches {:.0f}% from {}".format(
alertThresholdExceeded * 100, budget_display_name
)
print(f"############################################")
print(f"budget threshold is {alertThresholdExceeded}")
print(f"{title}")
print(f"{content}")
print(f"############################################")
if alertThresholdExceeded == 0.75:
send_noti(content, title)
elif alertThresholdExceeded == 1.0:
### do something differently from 75% ###
send_noti(content, title)
def send_noti(content, title):
## send noti to somewhere
แต่ปัญหาหลักคือ เจ้า Pub/sub ชอบส่ง msg มาซ้ำน่ะสิ
Budget notifications are sent to the Pub/Sub topic multiple times per day with the current status of your budget. This is a different cadence than the budget alert emails, which are only sent when a budget threshold is met
ซึ่งหากโจทย์เราคือ เราอยากให้ระบบส่ง noti มาแค่ 1 ครั้งต่อเดือน ตรงนี้ Cloud Functions เดี่ยวๆไม่สามารถเก็บได้ว่าโปรเจคไหนเคยส่ง เพราะหน้าที่หลักของ Cloud Functions คือเช็ค logic ของ threshold
ดังนั้นเราจึงเลือก database สักตัวที่ช่วยเราเก็บได้ว่า เคยส่ง noti ไปหรือยัง และส่งครั้งล่าสุดไปเมื่อไร ซึ่งเราเลือก Cloud Firestore มาช่วย เนื่องจากมัน simple ดี
ค้าง Cloud Functions ไว้แล้วขอสลับมาที่ Firestore กันแปบนึง
Cloud Firestore
ชะแว้บบบ เรามากันที่ Firestore
เราเลือกสร้างเป็น Native Mode และ เราจะสร้าง Collection ชื่อ budget-alert-notification-stats
หน้าตาของ Firestore เราจะหน้าตาคร่าวๆแบบนี้ คือ document ภายใน budget-alert-notification-stats
จะมีลิสของโปรเจค เช่น project_a, project_b ว่าเคยส่งโนติไปแล้ว แล้วใน document ก็จะเก็บว่าส่งโนติไปครั้งล่าสุดเมื่อไร
ตรงนี้ทุกคนอาจจะยังไม่มี project_a กับ project_b นะ สร้างแค่ collection ก็พอ แล้วกลับไปที่ Cloud Functions กัน
Back to Cloud Functions
คราวนี้เราจะ edit code ภายใน Cloud Functions เพิ่มเติมว่าให้ไปรู้จัก Cloud Firestore แล้วเช็คว่าเคยส่ง noti ไปแล้วหรือยังในเดือนนั้น
หากไม่ยังไม่เคยส่ง ก็ส่ง
แต่หากเคยส่งแล้ว ก็ไม่ต้องทำอะไรต่อ
# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def subscribe(cloud_event: CloudEvent) -> None:
# Print out the data from Pub/Sub, to prove that it worked
print(
"Hello, " + base64.b64decode(cloud_event.data["message"]["data"]).decode() + "!"
)
pubsub_data = base64.b64decode(cloud_event.data["message"]["data"]).decode("utf-8")
pubsub_json = json.loads(pubsub_data)
cost_amount = pubsub_json["costAmount"]
budget_amount = pubsub_json["budgetAmount"]
budget_display_name = pubsub_json["budgetDisplayName"]
alertThresholdExceeded = pubsub_json.get("alertThresholdExceeded", None)
project_id = get_alert_name(pubsub_json["budgetDisplayName"])
if alertThresholdExceeded is None:
if cost_amount <= budget_amount:
print(f"No action necessary. (Current cost: {cost_amount})")
return
else:
content = (
"""Budget name '{}'.Current cost is ${}.Budget cost is ${}💸 💸 💸 💸""".format(
budget_display_name, cost_amount, budget_amount
)
)
title = "💸 Cost reaches {:.0f}% from {}".format(
alertThresholdExceeded * 100, budget_display_name
)
print(f"############################################")
print(f"budget threshold is {alertThresholdExceeded}")
print(f"{title}")
print(f"{content}")
print(f"############################################")
alert_percent = int(alertThresholdExceeded * 100)
should_notify = check_to_notify_per_month(project_id, alert_percent)
if alertThresholdExceeded == 0.75 and should_notify:
send_teams(
webhook_url,
content=content,
title=title,
color=green_code,
)
elif alertThresholdExceeded == 1.0 and should_notify:
### do something differently from 75% ###
send_teams(
webhook_url,
content=content,
title=title,
color=red_code,
)
def check_to_notify_per_month(project_id: str, threshold_percent) -> bool:
"""_summary_
check if already notify in Teams in that month or not
by checking from Cloud Firestore
Args:
project_id (str): project id get from alert name
Returns:
bool: True when already notify and update data in Firestore, False to skip notify
"""
db = firestore.Client()
last_notify_ref = db.collection("budget-alert-notification-stats").document(
project_id
)
current_timestamp = pendulum.now("Asia/Bangkok")
try:
last_notify_snapshot = last_notify_ref.get()
data = last_notify_snapshot.to_dict()
if not last_notify_snapshot.exists:
# Initialize with the current timestamp if it's the first time.
last_notify_ref.set({f"last_noti_{threshold_percent}": current_timestamp})
print("Never has notification before, should notify team")
return True
elif f"last_noti_{threshold_percent}" not in data:
# Update existing doc with new threshold
last_notify_ref.update(
{f"last_noti_{threshold_percent}": current_timestamp}
)
print(
f"Never has notification for {threshold_percent} before, should notify team"
)
return True
else:
# Both threshold is in doc, check if it should notify
last_notify_in_bkk = convert_to_timestamp_with_bkk(
last_notify_snapshot.get(f"last_noti_{threshold_percent}")
)
print("last_notify_in_bkk: ", last_notify_in_bkk)
print("current_timestamp: ", current_timestamp)
# Check if a month has passed since the last notify.
if has_one_month_passed(current_timestamp, last_notify_in_bkk):
# Proceed with sending the notify to Microsoft Teams.
# Update the last notify timestamp.
last_notify_ref.update(
{f"last_noti_{threshold_percent}": current_timestamp}
)
return True
else:
print("Skipping notify, All ready notify in this month.")
return False
except Exception as e:
print("Error processing notify:", e)
def convert_to_timestamp_with_bkk(timestamp_from_firestore: datetime) -> datetime:
last_notify_timestamp = timestamp_from_firestore.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
last_notify_timestamp = pendulum.parse(last_notify_timestamp, tz="UTC")
last_notify_timestamp_in_bkk = last_notify_timestamp.in_tz("Asia/Bangkok")
return last_notify_timestamp_in_bkk
def has_one_month_passed(
current_timestamp: datetime, last_notify_timestamp: datetime
) -> bool:
current_month, current_year = get_month_and_year(current_timestamp)
last_notify_month, last_notify_year = get_month_and_year(last_notify_timestamp)
return (current_year > last_notify_year) or (
current_year == last_notify_year and current_month > last_notify_month
)
def get_month_and_year(timestamp: datetime):
month = timestamp.month
year = timestamp.year
return month, year
หน้าตาของ Cloud Functions จะหน้าตาประมาณนี้ เราใช้ python 3.11 มีไฟล์ main.py และ requirement.txt
เราไม่ได้ใส่โค้ดทั้งหมดเพราะเดี๋ยวจะยาวไป หากสนใจอ่านโค้ดเต็มๆแนะนำไปดูที่ Github
Test ว่า Cloud Functions ใช้ได้ไหม
เนื่องจากเราต้องใช้เวลาสักพักเพื่อรอให้ msg จะวิ่งมา ระหว่างนั้นเราสามารถทำการเทสโดยไม่ต้องรอ ให้เข้าไปที่ Pub/sub topic > messages > publish message
แล้วพ่น message ปลอมผ่าน topics ไปก่อน
{"test": "yes","budgetDisplayName": "mils-project-bigquery-notification","alertThresholdExceeded": 0.75,"costAmount": 730.0,"costIntervalStart": "2023-07-02T07:00:00Z","budgetAmount": 1.0,"budgetAmountType": "SPECIFIED_AMOUNT","currencyCode": "THB"}
Notifications เราเด้งแล้ว ก็แปลว่า Cloud Functions ใช้ได้
แต่เทสแล้ว อย่าลืมไปลบ document ใน Cloud Firestore ด้วยนะ เดี๋ยวมันจะไม่ส่งมาใหม่5555
ผลลัพท์ของการ setup
1. Cloud Pub/sub Topics
หากเราปล่อยระบบทิ้งไว้สักพัก ภายใน Cloud Pub/Sub topics มันจะมี msg วิ่งมา หน้าตาจะคล้ายๆแบบนี้
2. Cloud Firestore
ใน Firestore ก็จะมี document ชื่อ project และข้างในจะมี field ชื่อ last_noti_100
หรือ/และ last_noti_75
ในเคสนี้มีแต่ last_noti_100
เพราะ cost ในเดือนนี้ดันพุ่งเกิน budget ไปที่เรียบร้อยแล้ว
3. Noti channel
ผลสุดท้ายเราจะได้รับ noti ที่แตกต่างกันออกไป ตามที่ตั้งใจไว้
เมื่อ cost ถึง 50% -> ได้รับ email เพียงอย่างเดียว
เมื่อ cost ถึง 75% -> ได้รับ email และ noti เข้า Teams
เมื่อ cost ถึง 100% -> ได้รับ email และ noti เข้า Teams (และอื่นๆแล้วแต่จะเพิ่ม)
ทิ้งท้าย
อันที่จริงแล้ว Flow หลักที่จำเป็นคือ Cloud Billing > Cloud Pub/Sub > Cloud Functions
ส่วนของ Firestore จะเป็นตัวเสริมเนื่องจากเราต้องการให้ระบบส่ง noti แค่ 1 ครั้งต่อเดือนเท่านั้น ซึ่งนั่นแล้วแต่ว่าผู้วางระบบต้องการออกแบบให้ flow เป็นเช่นไร
อย่าง threshold ที่ 100% เราก็อาจจะปรับให้ BigQuery สลับไปใช้ Editions pricing plan แบบ auto ก็ได้ (อีกชื่อคือ assign reservations) ก็จะเป็นการ custom ที่เราหวังไว้ แต่ไม่ได้ใส่ในบทความหรือโค้ดตัวอย่าง เนื่องจากเราอยากเน้นให้เห็นการทำงานหลักของ 3 ตัวนี้ ซึ่งผู้อ่านสามารถ custom ได้ตามสะดวก
และในกรณีมี project เข้ามาใหม่ ก็เพียงแค่สร้าง budget & alerts ใน Cloud Billing เพิ่ม แล้วผูกกับ Pubsub topics เดิมที่เซ็ตไว้ ก็ใช้ได้แล้ว