AWS Organization下アカウント課金情報をSlackに投稿する
目次
前提
- SlackとのWebhook連携は完了していて Incoming Webhook は既に払い出されているものとします。
- ServerlessFrameworkとSlackに関しての詳しい説明はしません。
.
├── handler.py
└── serverless.yml
ce_args = {
'TimePeriod': {
'Start': str(today - timedelta(days=2)),
'End': str(today),
},
'Granularity': 'DAILY',
'Metrics': ['UnblendedCost'],
'GroupBy': [{
'Type': 'DIMENSION',
'Key': 'LINKED_ACCOUNT',
}]
}
ce_client = boto3.client('ce')
ce_response = ce_client.get_cost_and_usage(**ce_args)
{
"DimensionValueAttributes": [
{
"Attributes": {
"description": "ACCOUNT_A" # アカウント名
},
"Value": "123456789012" # アカウントID
},
{
"Attributes": {
"description": "ACCOUNT_B"
},
"Value": "123456789013"
},
.....
],
.....
"ResultsByTime": [
{ # 2021-08-28 のアカウント毎の課金情報
"Estimated": true,
"Groups": [
{
"Keys": [
"123456789012"
],
"Metrics": {
"UnblendedCost": {
"Amount": "6.6658078778",
"Unit": "USD"
}
}
},
{
"Keys": [
"123456789013"
],
"Metrics": {
"UnblendedCost": {
"Amount": "0.164979503",
"Unit": "USD"
}
}
},
.....
],
"TimePeriod": {
"End": "2021-08-29",
"Start": "2021-08-28"
},
.....
},
{ # 2021-08-29のアカウント毎の課金情報
"Estimated": true,
"Groups": [
{
"Keys": [
"123456789012"
],
"Metrics": {
"UnblendedCost": {
"Amount": "6.6658078778",
"Unit": "USD"
}
}
},
{
"Keys": [
"123456789013"
],
"Metrics": {
"UnblendedCost": {
"Amount": "0.164979503",
"Unit": "USD"
}
}
},
.....
],
"TimePeriod": {
"End": "2021-08-30",
"Start": "2021-08-29"
},
.....
},
]
}
def create_ce_url(account_id):
# 1週間前から昨日までの課金情報を表示
start = today - timedelta(days=7)
end = today - timedelta(days=1)
base_url = 'https://console.aws.amazon.com/cost-management/home?#/custom'
filter_option = [{
'dimension': 'LinkedAccount',
'values': [account_id],
'include': True,
'children': None
}]
filter = json.dumps(filter_option, separators=(',', ':')).encode('utf-8')
# 以下のクエリパラメータは実際にマネジメントコンソールのCostExploreを操作した
# 結果付与されるクエリパラメータを参考にしてください。
query_params = {
'groupBy': 'Service',
'hasBlended': 'false',
'hasAmortized': 'false',
'excludeDiscounts': 'true',
'excludeTaggedResources': 'false',
'excludeCategorizedResources': 'false',
'excludeForecast': 'false',
'timeRangeOption': 'Last7Days',
'granularity': 'Daily',
'reportName': ' ',
'reportType': 'CostUsage',
'isTemplate': 'true',
'filter': filter,
'chartStyle': 'Stack',
'forecastTimeRangeOption': 'None',
'usageAs': 'usageQuantity',
'startDate': str(start),
'endDate': str(end)
}
slack_attachments = []
for groups in ce_response['ResultsByTime']:
target_date = groups['TimePeriod']['Start']
total = 0
slack_fields = []
for group in groups['Groups']:
cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
account_id = group["Keys"][0]
ce_url = create_ce_url(account_id)
# slack mrkdwnのlinkフォーマットでアカウント名を表示
slack_fields.append({
"title": accounts_dict[account_id],
"value": "<{0}|{1:.2f}USD>".format(ce_url, cost),
"short": True
})
# 日毎の総課金額
total += cost
slack_attachments.append({
'color': "#36a64f",
'fields': slack_fields,
# 全AWSアカウントを合算した金額を表示
'pretext': "{0}のトータルAWS料金は約{1:.2f}USDです".format(target_date, total)
})
send_slack(slack_attachments)
def send_slack(attachments):
messages = {"attachments": attachments}
req = Request(SLACK_WEBHOOK_URL, json.dumps(messages).encode('utf-8'))
try:
urlopen(req)
except HTTPError as e:
print("Request failed: %d %s" % (e.code, e.reason))
except URLError as e:
print("Connection failed: %s" % (e.reason))
import json
import os
from datetime import date, timedelta
from urllib.parse import urlencode, quote
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
import boto3
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
today = date.today()
def lambda_handler(event, context):
ce_args = {
'TimePeriod': {
'Start': str(today - timedelta(days=2)),
'End': str(today),
},
'Granularity': 'DAILY',
'Metrics': ['UnblendedCost'],
'GroupBy': [{
'Type': 'DIMENSION',
'Key': 'LINKED_ACCOUNT',
}]
}
ce_client = boto3.client('ce')
ce_response = ce_client.get_cost_and_usage(**ce_args)
accounts_dict = {}
for a in ce_response['DimensionValueAttributes']:
accounts_dict[a['Value']] = a['Attributes']['description']
slack_attachments = []
for groups in ce_response['ResultsByTime']:
target_date = groups['TimePeriod']['Start']
total = 0
slack_fields = []
for group in groups['Groups']:
cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
account_id = group["Keys"][0]
ce_url = create_ce_url(account_id)
# slack mrkdwnのlinkフォーマットでアカウント名を表示する。
slack_fields.append({
"title": accounts_dict[account_id],
"value": "<{0}|{1:.2f}USD>".format(ce_url, cost),
"short": True
})
total += cost
slack_attachments.append({
'color': "#36a64f",
'fields': slack_fields,
'pretext': "{0}のトータルAWS料金は約{1:.2f}USDです".format(target_date, total)
})
send_slack(slack_attachments)
def create_ce_url(account_id):
start = today - timedelta(days=7)
end = today - timedelta(days=1)
base_url = 'https://console.aws.amazon.com/cost-management/home?#/custom'
filter_option = [{
'dimension': 'LinkedAccount',
'values': [account_id],
'include': True,
'children': None
}]
filter = json.dumps(filter_option, separators=(',', ':')).encode('utf-8')
query_params = {
'groupBy': 'Service',
'hasBlended': 'false',
'hasAmortized': 'false',
'excludeDiscounts': 'true',
'excludeTaggedResources': 'false',
'excludeCategorizedResources': 'false',
'excludeForecast': 'false',
'timeRangeOption': 'Last7Days',
'granularity': 'Daily',
'reportName': ' ',
'reportType': 'CostUsage',
'isTemplate': 'true',
'filter': filter,
'chartStyle': 'Stack',
'forecastTimeRangeOption': 'None',
'usageAs': 'usageQuantity',
'startDate': str(start),
'endDate': str(end)
}
return base_url + '?' + urlencode(query_params, safe=':,', quote_via=quote)
def send_slack(attachments):
messages = {"attachments": attachments}
req = Request(SLACK_WEBHOOK_URL, json.dumps(messages).encode('utf-8'))
try:
urlopen(req)
except HTTPError as e:
print("Request failed: %d %s" % (e.code, e.reason))
except URLError as e:
print("Connection failed: %s" % (e.reason))
デプロイ
service: aws-billing-bot
provider:
name: aws
region: ap-northeast-1
runtime: python3.8
stage: ${opt:stage, "prod"}
#
iam:
role:
statements:
- Effect: "Allow"
Action:
- "ce:GetCostAndUsageWithResources"
- "ce:GetCostAndUsage"
Resource: "*"
environment:
SLACK_WEBHOOK_URL: "slackのwebhookURL"
package:
include:
- 'handler.py'
functions:
main:
handler: handler.lambda_handler
events:
#
- schedule: cron(0 1 * * ? *)
デプロイコマンド
$ serverless deploy