วิธีการแจ้งเตือนค่าใช้จ่ายการใช้งาน AWS ใน Slack ทุกวัน
สวัสดีครับ POP จากบริษัท Classmethod (Thailand) ครับ
ผู้ที่ใช้งานบริการ AWS ส่วนใหญ่ มีการควบคุมค่าใช้จ่ายการใช้งาน AWS ใช่ไหมครับ?
บางครั้งเราอาจหลีกเลี่ยงปัญหาที่ทำให้สิ้นเปลืองค่าใช้จ่ายไม่ได้ เนื่องจากลืม Stop หรือลบ Resource ที่ไม่ได้ใช้งานนั่นเอง
วันนี้เราจะมาพูดถึงเรื่องเหล่านี้ และแนะนำการตั้งค่าแจ้งเตือนค่าใช้จ่ายการใช้งาน AWS ใน Slack ทุกวันตามเวลาที่กำหนด เนื่องจากการตั้งค่าเหล่านี้จะช่วยให้เราสามารถตรวจสอบค่าบริการ และมองเห็นค่าใช้จ่ายการใช้งาน AWS ในแต่ละวันได้อย่างชัดเจนมากขึ้น
บทนำ
ใน AWS Account ของสำนักงานในประเทศไทยที่ใช้ทดสอบนี้ อาจมีบางครั้งที่ค่าบริการเพิ่มขึ้นมากเกินไป เนื่องจากลืม Stop Instance โดยไม่ได้ตั้งใจ (เป็นสิ่งที่ไม่ควรเกิดขึ้น)
จึงอยากจะมาแนะนำตั้งค่าการแจ้งเตือนค่าใช้จ่ายการใช้งาน AWS ในแต่ละวันเพื่อตรวจสอบค่าบริการทั้งหมดในบัญ AWS Account ครับ
นอกจากนี้ เนื้อหาในบทความนี้ได้ปรับเปลี่ยนมาจากเนื้อหาตามลิงก์ด้านล่างนี้ ต้องขอขอบคุณ คุณฟูจิอิ(藤井元貴) ด้วยครับ
สิ่งที่จะทำ
เป็นการสร้างและกำหนดค่าแบบไร้เซิร์ฟเวอร์ (Serverless) ที่เปิดใช้งาน Lambda โดยตั้งค่าให้เป็นเวลาที่กำหนดทุกวัน แล้ว Lambda จะได้รับค่าบริการจาก AWS Cost Explorer และโพสต์ไปยัง Slack ทุกวันตามเวลาที่กำหนดไว้ใน Lambda ครับ
ข้อควรระวัง
โปรดทราบว่าค่าบริการที่มีการคำนวณออกมาของตัวอย่างนี้ อาจไม่ตรงกับจำนวนเงินที่เรียกเก็บจริง
เมื่อทำการตั้งค่านี้จะทำให้มีค่าใช้จ่ายประมาณ 0.6 USD/เดือน เนื่องจากเป็นค่าใช้จ่ายในการเรียกใช้ Cost Explorer API ครับ
ดูรายละเอียดค่าบริการ Cost Explorer API เพิ่มเติมได้ที่ลิงก์ด้านล่างนี้
ตั้งค่า AWS Cost Explorer
ก่อนอื่นให้เข้าไปที่บริการ AWS Cost Explorer ใน AWS Management Console แล้วเลือก Cost Explorer
หากแสดงข้อความว่าไม่ได้รับอนุญาต ให้เข้าสู่ระบบด้วยบัญชี Root (Root Account) และเปิดใช้งาน Cost Explorer
จากหน้าจอตั้งค่าครับ
ดูรายละเอียดเพิ่มเติมได้ที่ลิงก์ด้านล่างนี้ครับ
ตั้งค่า Slack
สร้าง Channel
เราจะสร้าง Channel ใน Slack โดยใช้ชื่อ aws_usage
และจะตั้งค่าเป็น Private เพราะเป็นข้อมูลเกี่ยวกับค่าบริการ
ก่อนอื่นเข้ามาที่หน้าจอ Slack ของเรา แล้วคลิก + Add channels
แล้วเลือก Create a new channel
ป้อนชื่อ Channel ที่ต้องการ เช่น aws_usage
แล้วคลิก Next
ครั้งนี้จะตั้งค่าเป็น Private
เพราะเป็นข้อมูลเกี่ยวกับค่าบริการ
หากยังไม่ต้องการเพิ่ม Email เพื่อนร่วมงาน ให้คลิก ✕
หรือ Skip for now
ได้เลย เพราะสามารถเพิ่มในภายหลังได้
เพิ่ม Incoming Webhook
เมื่อสร้าง Channel เสร็จแล้วจะแสดงหน้าจอแบบนี้ ผมจะใช้ Channel นี้รับการแจ้งเตือนจาก Lambda function
จากนั้นให้คลิกที่ชื่อ Channel ของเรา
เลือกแท็บ Integrations
และคลิก Add an App
ค้นหาและเลือกแอป Incoming Webhook
แล้วจะย้ายไปที่หน้าจอบนเว็บเบราว์เซอร์โดยอัตโนมัติ
เลือก Channel aws_usage
ที่สร้างเมื่อสักครู่นี้ที่ Post to Channel และคลิก Add Incoming WebHooks integration
แล้วจดบันทึก Webhook URL ที่สร้างขึ้นเตรียมไว้ และบันทึกการตั้งค่านี้โดยเลื่อนลงมาด้านล่างสุดแล้วคลิก Save Settings
สร้าง Lambda Layer
ให้สร้าง Lambda Layer เพื่อใช้ Requests library ใน Python กับ Lambda function นี้ครับ
ดูตัวอย่างได้ที่ลิงก์นี้ครับ
ครั้งนี้จะสร้าง Lambda Layer ตามด้านล่างนี้
ตัวอย่างตั้งค่าการสร้าง Lambda Layer สำหรับครั้งนี้
Layer configuration
Name:python-requests
เลือก
◎ Upload a file from Amazon S3
Amazon S3 link URL:Your S3 URI
Compatible architectures:
✅ x86_64
Compatible runtimes:
Python 3.11
Python 3.10
Python 3.7
Python 3.8
Python 3.9
(เลือกเฉพาะ Python)คลิก
Create
สร้าง Lambda function
เข้ามาที่หน้าจอ AWS Lambda ใน AWS Management Console แล้วทำการสร้าง Lambda function ดังนี้
แล้วจะทำการตั้งค่าหน้า Create function ตามนี้
◎ Author from scratch
Basic information
Function name: aws_usage_notify
(ป้อนชื่อที่ต้องการ)
Runtime: Python 3.11
Architecture: ◎ x86_64
เมื่อสร้างเสร็จแล้วจะแสดงหน้าจอแบบนี้
ตั้งค่า Configuration
General configuration
เลื่อนลงมาด้านล่าง เลือกแท็บ Configuration
แล้วเลือกเมนู General configuration
แล้วคลิก Edit
ตั้งค่า Timeout เป็น [0 min 30
sec] แล้วคลิก Save
Permissions
การตั้งค่าในส่วนของ IAM Role นี้ เป็นการเพิ่มสิทธิ์ Cost Explorer ให้กับ Role name ที่ Lambda function ใช้งานอยู่ เพื่อให้ Lambda function สามารถเข้าถึง Cost Explorer และรับข้อมูลไปแจ้งเตือนใน Slack ได้ครับ
เลือกแท็บ Configuration
แล้วเลือกเมนู Permissions
แล้วคลิก Role name
ของเรา แล้วจะย้ายไปหน้าจอของ Role name นี้โดยอัตโนมัติ
เมื่อย้ายมาหน้าจอ Role name ของเราแล้ว คลิก Add permissions
และเลือก Create inline policy
Step 1 - Specify permissions
เลือก JSON
แล้วลบ Policy เก่าออกทั้งหมด แล้วคัดลอก Policy ด้านล่างนี้วางแทนที่ก็จะแสดงตามรูปภาพ และเลื่อนลงมาด้านล่าง แล้วคลิก Next
Policy นี้เป็น Policy สำหรับอนุญาตการเชื่อมต่อจาก Lambda Function ไปยัง Cost Explorer API
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ce:*" ], "Resource": [ "*" ] } ] }
Step 2 - Review and create
ป้อน Policy name: CostExplorerPolicy
(ชื่ออะไรก็ได้) แล้วคลิก Create policy
แล้วดูในแท็บ Permissions จะเห็นว่ามี Policy name: CostExplorerPolicy
ที่สร้างขึ้นมาแล้ว
เพิ่ม Code
กลับมาที่หน้าจอ Lambda function แล้วเลือกแท็บ Code
แล้วดูที่แท็บ lambda_function
ใน Code source
ให้ลบ Code เก่าออกทั้งหมด แล้วคัดลอก Code ด้านล่างนี้วางแทนที่ก็จะแสดงตามรูปภาพด้านล่าง Code นี้
แล้วคลิก Deploy
เพื่อบันทึก (อย่าลืมเปลี่ยน [your-webhook-url] ให้เป็น Webhook URL ของคุณ)
import os
import boto3
import json
import requests
from datetime import datetime, timedelta, date
from pprint import pprint
SLACK_WEBHOOK_URL = '[your-webhook-url]'
def lambda_handler(event, context) -> None:
client = boto3.client('ce', region_name='us-east-1')
daily_billings = get_total_billing(client)
# pprint(daily_billings)
total, service_billings = get_service_billings(client)
# pprint(service_billings)
(title, detail) = get_message(daily_billings, service_billings, total)
# pprint(title)
# pprint(detail)
post_slack(title, detail)
def get_total_billing(client) -> dict:
(start_date, end_date) = get_3days_date_range()
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage
response = client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='DAILY', # 'DAILY'|'MONTHLY'|'HOURLY'
Metrics=[
'AmortizedCost'
],
)
daily_billings = []
for item in response['ResultsByTime']:
daily_billings.append({
'date': item['TimePeriod']['Start'],
'billing': float(item['Total']['AmortizedCost']['Amount'])
})
return daily_billings
def get_service_billings(client) -> list:
(start_date, end_date) = get_this_month_date_range()
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage
response = client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='MONTHLY', # 'DAILY'|'MONTHLY'|'HOURLY'
Metrics=[
'AmortizedCost'
],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'SERVICE'
}
]
)
billings = []
total = 0.0
for item in response['ResultsByTime'][0]['Groups']:
billings.append({
'service_name': item['Keys'][0],
'billing': float(item['Metrics']['AmortizedCost']['Amount'])
})
total += float(item['Metrics']['AmortizedCost']['Amount'])
return total, billings
def get_message(daily_billings: list, service_billings: list, total: float) -> (str, str):
(start_date, end_date) = get_this_month_date_range()
end_today = datetime.strptime(end_date, '%Y-%m-%d')
end_yesterday = (end_today - timedelta(days=1)).strftime('%m/%d')
title = 'AWS Usage Quick Report\n'
for item in daily_billings:
date = item['date']
billing = item['billing']
title += f'{date}: {billing:.2f} USD\n'
title += f'\nTotal: {total:.2f} USD ({start_date} - {end_yesterday})\n'
details = []
for item in service_billings:
service_name = item['service_name']
billing = round(float(item['billing']), 2)
if billing == 0.0:
continue
details.append(f' * {service_name}: {billing:.2f} USD')
return title, '\n'.join(details)
def post_slack(title: str, detail: str) -> None:
# https://api.slack.com/incoming-webhooks
# https://api.slack.com/docs/message-formatting
# https://api.slack.com/docs/messages/builder
payload = {
'attachments': [
{
'color': '#36a64f',
'pretext': title,
'text': detail
}
]
}
# http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/
try:
response = requests.post(SLACK_WEBHOOK_URL, data=json.dumps(payload))
except requests.exceptions.RequestException as e:
print(e)
else:
print(response.status_code)
def get_3days_date_range() -> (str, str):
start_date = get_prev_day(3)
end_date = get_prev_day(0)
return start_date, end_date
def get_this_month_date_range() -> (str, str):
start_date = get_begin_of_month()
end_date = get_today()
if start_date == end_date:
end_of_month = datetime.strptime(start_date, '%Y-%m-%d') + timedelta(days=-1)
begin_of_month = end_of_month.replace(day=1)
return begin_of_month.date().isoformat(), end_date
return start_date, end_date
def get_begin_of_month() -> str:
return date.today().replace(day=1).isoformat()
def get_prev_day(prev: int) -> str:
return (date.today() - timedelta(days=prev)).isoformat()
def get_today() -> str:
return date.today().isoformat()
เพิ่ม Lambda Layer
มาที่หัวข้อ Function overview ด้านบน แล้วคลิก Layers (0)
มาที่หัวข้อ Layers แล้วคลิก Add a layer
แล้วจะทำการตั้งค่าหน้า Add layer ดังนี้
Choose a layer
Layer source: ◎ Custom layers
Custom layers: python-requests
Version: 1
คลิก Add
เมื่อเพิ่ม Lambda Layer เสร็จแล้ว จะแสดง Layers (1)
แบบนี้
ทดสอบส่งแจ้งเตือน
เราจะทำการทดสอบส่งแจ้งเตือนจาก Lambda function ไปที่ Channel ปลายทางใน Slack ที่สร้างในตอนแรก
เลือกแท็บ Test
และคลิก Test
แล้วจะแสดงข้อความ Executing function: succeeded แบบนี้
หากต้องการดูรายละเอียดเพิ่มเติม คลิกที่ Log หรือ Details ได้ครับ
แล้วเปิด Channel ปลายทางใน Slack ของเรา จะเห็นว่ามีการส่งแจ้งเตือนค่าใช้จ่ายการใช้งาน AWS มาแล้วครับ
ตั้งค่า Trigger
เราจะทำการตั้งค่า Trigger เพื่อให้ Lambda function ส่งแจ้งเตือนไปที่ Channel ปลายทางใน Slack ของเราตามเวลาที่กำหนดทุกวันโดยอัตโนมัติครับ
มาที่หัวข้อ Function overview ด้านบน แล้วคลิก Add trigger
แล้วจะทำการตั้งค่าหน้า Add trigger ดังนี้
Trigger configuration
ค้นหาและเลือก EventBridge (CloudWatch Events)
Rule: ◎ Create a new rule
Rule name: Daily_8AM
(ป้อนชื่อที่ต้องการ)
Rule description: (ป้อนตามต้องการ)
Rule type: ◎ Schedule expression
Schedule expression: cron(0 1 ? * * *)
(การตั้งค่านี้เป็นเวลาเท่ากับ 08:00
ของเขตเวลาในประเทศไทย (GMT+7))
คลิก Add
เมื่อตั้งค่า Trigger เสร็จแล้ว จะแสดง EventBridge (CloudWatch Events)
แบบนี้ครับ
ผลลัพธ์การตั้งค่า Trigger สำหรับตัวอย่างนี้
เมื่อทำการตั้งค่า Trigger เสร็จแล้ว Lambda function จะส่งแจ้งเตือนไปที่ Channel ปลายทางที่ชื่อaws_usage
ใน Slack ทุกวันตามเวลาที่กำหนดไว้คือ08:00
ของเขตเวลาในประเทศไทย (GMT+7) โดยอัตโนมัติครับ
เกี่ยวกับการแจ้งเตือน
การแจ้งเตือนที่ได้รับมาในแต่ละวันอาจจะมีผลลัพธ์ที่ไม่ตรงกัน เนื่องจากการเก็บข้อมูลจาก Cost Explorer มีการดีเลย์และใน 1 วันมีการอัพเดทหลายรอบ เช่น ผลลัพธ์ที่แสดงขึ้นจะเป็นของเมื่อวานซึ่งทยอยทำการอัพเดทตามช่วงเวลานั้นๆ จึงทำให้ผลลัพธ์อาจจะมีการเปลี่ยนแปลงขึ้นได้
Channel [aws_usage] ใน Slack
# ผลลัพธ์การแจ้งเตือนของวันที่ 2023-08-10
incoming-webhook APP 2:37 PM
AWS Usage Quick Report
2023-08-07: 9.67 USD
2023-08-08: 7.16 USD
2023-08-09: 6.21 USD
# ผลลัพธ์การแจ้งเตือนของวันที่ 2023-08-11
incoming-webhook APP 8:00 AM
AWS Usage Quick Report
2023-08-08: 7.23 USD
2023-08-09: 8.65 USD
2023-08-10: 4.38 USD
สุดท้ายนี้
เราสามารถส่งแจ้งเตือนค่าใช้จ่ายการใช้งาน AWS ไปยัง Slack ในแต่ละวันได้โดยทำการตั้งค่านี้
หากเราตรวจสอบค่าบริการทุกวัน จะช่วยให้เราเข้าใจเกี่ยวกับค่าใช้จ่ายการใช้งาน AWS ได้เป็นอย่างดี และช่วยให้เราควบคุมการใช้งานต่างๆ ได้มากขึ้น เช่น ไม่ลืมปิดใช้งาน Resource ที่ไม่ได้มีการใช้งานตลอดเวลา หรือ ไม่ลืมลบ Resource ที่ไม่จำเป็นออกไป เป็นต้น ถ้าเราสามารถควบคุมสิ่งเหล่านี้ได้จะทำให้เราประหยัดต้นทุนได้เป็นอย่างดีครับ
ผมหวังว่าบทความนี้จะเป็นประโยชน์ให้กับผู้อ่านได้นะครับ
บทความต้นฉบับ
เรียบเรียงโดย: คุณมินามิ(Keisuke Minami) ประธานบริษัท Classmethod (Thailand)
เขียนโดย: ป๊อป(Tinnakorn Maneewong) จากบริษัท Classmethod (Thailand)