はじめに
本記事は「こんなことやってみた」的な記事となります。
やること
AWSのCost ExplorerのAPIに、Pythonで問い合わせを行い、その結果をSlackに通知します。
通知する内容は、1週間の1日ごとのサービスの料金になります。
Lambdaで動かすことを想定してプログラムを書いてみました。(自分はPython初心者です。コードの書き方などについては多めに見ていただけると幸いです)
通知される結果(文章)
先に通知される結果の文章を共有します。ご興味ある方は続きを読んでいただければと思います。
※単位はUSDとなっております。
ターミナル
###############################
### 2023-02-03 - 2023-02-10 ###
###############################
SERVICE_NAME 2023-02-03 2023-02-04 2023-02-05 2023-02-06 2023-02-07 2023-02-08 2023-02-09
AWS Lambda 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
Amazon DynamoDB 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
Amazon EC2 Container Registry (ECR) 0.000550 0.000550 0.000550 0.000550 0.000550 0.000550 0.000550
EC2 - Other 0.034446 0.034446 0.034446 0.034446 0.034446 0.034446 0.008725
Amazon Relational Database Service 0.003812 0.003812 0.003812 0.003812 0.003812 0.003812 0.003812
Amazon Route 53 0.000125 0.000084 0.000086 0.000191 0.000071 0.000088 0.000310
Amazon Simple Storage Service 0.000422 0.000416 0.000417 0.000416 0.000416 0.000500 0.000448
AmazonCloudWatch 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
CloudWatch Events 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
Amazon API Gateway 0.000000 0.000000 0.000007 0.000003 0.000000 0.000000 0.000000
AWS Cost Explorer 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.780000
###############################
Slack
(補足)Cost Explorerでは
Cost Explorerでは小数点第2位までの表示ですので、自分が作成した小数点第6位まで表示するものとは少し表示に差異がありました。
取り組んだ背景
概ね以下のような理由からです。
- AWSのAPIを触ってみたかった。
- 毎日Cost Explorerでコストを確認するのが面倒で忘れることもある。なので自動化できないかと考え始めた。
- この通知を行うことによって、チームにコストを共有でき、料金が跳ね上がっているところがあったら即座に調査を開始できる。
プログラム
import boto3
import json
client = boto3.client('ce')
from datetime import datetime, timedelta
import slackweb
def get_days():
"""
昨日と8日前の日付をYYYY-MM-DD形式で取得する。
Returns
-------
day1 : str
昨日の日付。
day8 : str
8日前の日付。
"""
day1 = (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d')
day8 = (datetime.today() - timedelta(days=8)).strftime('%Y-%m-%d')
return day1, day8
def lambda_handler(event, context):
day1, day8 = get_days()
response_daily = client.get_cost_and_usage(
TimePeriod={
'Start': day8,
'End' : day1,
},
Granularity='DAILY',
Metrics=[
'NetUnblendedCost',
],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'SERVICE'
},
],
)
column_name = "SERVICE_NAME" + "\t"
services_amounts = {} # サービスごとの料金を格納する。ex) {'AWS Lambda': [1, 2, 3, 4, 5, 6, 7]}
amount_no = 0 # services_amountsのリスト型valueの場所を指定する番号。レポートの1日目は0。2日目は1、3日目は2...とインクリメントする。
# Slackに投稿する文章を作成するための変数たち。
service_name = "" # サービスの名称を格納する。
amount_list = [] # サービスごとの7日間のそれぞれの料金を格納する。
slack_report = "" # Slackに投稿する文章を格納する最終的な文字列変数。
for weekly_report in response_daily['ResultsByTime']:
for daily_report in weekly_report.items():
if daily_report[0] == 'TimePeriod':
# ここではStartの日付だけを取得する。(StartとEndがある)
# column_nameは 「SERVICE_NAME 2023-02-01 2023-02-02 ...」というかたちになる。(1週間の日付が並ぶ)
column_name = column_name + daily_report[1]['Start'] + "\t"
elif daily_report[0] == 'Groups':
for service_amount in daily_report[1]: # daily_report[1]にはサービスごとの料金(1日分)が格納されている。
if service_amount['Keys'][0] not in services_amounts:
# サービスごとの料金を格納するdict変数を作成する。(ex:{'AWS Lambda', [1, 2, 3, 4, 5, 6, 7]})
# 7日分の料金を格納するので、あらかじめ0.000000で7要素のリスト型のvalueとする。
services_amounts[service_amount['Keys'][0]] = ['{:.6f}'.format(0.000000)] * 7
# 料金1日分をサービスごとに作成したdict変数のリスト型valueに格納する。何日目の料金であるかamount_noで判断してinsertする。
# 小数点は6桁まで表示することにする。
services_amounts[service_amount['Keys'][0]].insert(amount_no, '{:.6f}'.format(float(service_amount['Metrics']['NetUnblendedCost']['Amount'])))
amount_no += 1
# Slackに投稿する文章を作成する。
slack_report = column_name # カラムを追記する。ex) SERVICE_NAME 2023-02-01 2023-02-02 ...
for i in services_amounts.items():
service_name = i[0]
amount_list = i[1]
slack_report = slack_report + """
{service_name}\t{amount1}\t{amount2}\t{amount3}\t{amount4}\t{amount5}\t{amount6}\t{amount7}""".format(service_name=i[0],
amount1=i[1][0],
amount2=i[1][1],
amount3=i[1][2],
amount4=i[1][3],
amount5=i[1][4],
amount6=i[1][5],
amount7=i[1][6])
start_char = ("###############################" + "\n"
"### {0} - {1} ###".format(day8, day1) + "\n"
+ "###############################" + "\n"
)
end_char = "\n" + "###############################"
slack_report = start_char + slack_report + end_char
# Slackに投稿する
slack = slackweb.Slack(url="https://hooks.slack.com/services/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")
slack.notify(text=slack_report)
print(slack_report)
Lambdaで動かす
Slackwebダウンロード
Slackに通知するためのslackwebライブラリをローカルのpythonディレクトリにダウンロード。
% mkdir python
% pip install slackweb -t python
Collecting slackweb
Using cached slackweb-1.0.5.tar.gz (1.3 kB)
Using legacy 'setup.py install' for slackweb, since package 'wheel' is not installed.
Installing collected packages: slackweb
Running setup.py install for slackweb ... done
Successfully installed slackweb-1.0.5
% zip -r layer.zip python
レイヤー作成
先ほどダウンロードしたslackwebをzipにしてAWS CLIでレイヤーを作成しました。
aws lambda publish-layer-version \
--layer-name slackweb \
--zip-file fileb://layer.zip \
--compatible-runtimes python3.9
関数作成
AWSコンソールから行いました。
項目 | 値 |
---|---|
ランタイム | Python 3.9 |
アーキテクチャ | x86_64 |
ハンドラ | lambda_function.lambda_handler |
レイヤー | 先ほど作成したものを指定します |
Lambdaにアタッチしたポリシー
プラスの箇所だけ追記しました。Cost ExplorerのCost and Usageを読み込む権限です。
{
"Version": "2012-10-17",
"Statement": [
+ {
+ "Sid": "CostExplorer",
+ "Effect": "Allow",
+ "Action": "ce:GetCostAndUsage",
+ "Resource": "*"
+ },
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:738452829225:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:738452829225:log-group:/aws/lambda/CheckCostExplorer:*"
]
}
]
}
最後に
これでEventBridgeなどを使用して、スケジューリングして動かせば毎日Cost Explorerにコンソールでアクセスしなくていいかもしれません。
ご自由にカスタマイズしてご使用いただければ幸いです。
参考サイト