何をするか
- AWSの請求金額をLINE、Slackに通知する
- Totalで1~2時間くらいあればできる
事前準備
-
予想 AWS 請求額をモニタリングする請求アラートを有効にする
- ルートユーザでログインしてチェック入れるだけ
- これでCloudWatchから請求額を取得することができる
- LINEのmessagingAPIを有効化し、アクセストークンとユーザIDを取得
- SlackのIncoming Webhooksを有効化し、Webhook URLを取得
簡単な説明
- 以下の処理をするLambda関数を作成
- 請求情報の取得
- LINEに請求情報を送信
- Slackに請求情報を送信
- CloudWatch の CloudWatch Events で、通知するLambda関数と通知タイミングを設定
ハマりポイント(あとで出てきます)
- 請求情報を取得する時のRegion
- Lambda に設定する実行ロール
- LINE messagingAPI へのPOSTデータの型
- CloudWatchのcron式
作成したLambda関数(順に説明します)
index.py
import boto3
import json
import datetime
import os
import urllib.request
_env = os.environ
#メイン処理
def lambda_handler(event, context):
#請求情報取得
_billing = getBillingMetric()
_billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ!"
#LINEに送信
sendNoticeToLine(_billing)
#Slackに送信
sendNoticeToSlack(_billing)
def getBillingMetric():
_cw = boto3.client('cloudwatch', region_name=_env["REGION"])
_billing = _cw.get_metric_statistics(
Namespace='AWS/Billing',
MetricName='EstimatedCharges',
Dimensions=[{'Name': 'Currency', 'Value': 'USD'}],
StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
EndTime=datetime.datetime.today(),
Period=86400,
Statistics=['Maximum'])
print(_billing)
return _billing
def sendNoticeToLine(_bill):
_url = 'https://api.line.me/v2/bot/message/push'
_data = json.dumps({
"to": _env["LINE_USER_ID"]
,"messages": [{"type": "text", "text": _bill["message"]}]
}).encode()
_header = {
"Content-type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
}
print(_data)
sendRequest(_url, _data, _header)
def sendNoticeToSlack(_bill):
_url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
_data = json.dumps({
"text": _bill["message"]
}).encode()
_header = {
"Content-type": "application/json"
}
print(_data)
sendRequest(_url, _data, _header)
def sendRequest(_url, _data, _header):
_req = urllib.request.Request(_url, _data, _header)
try:
with urllib.request.urlopen(_req) as _res:
_body = _res.read()
print(_body)
except urllib.error.HTTPError as _err:
print("HTTPError: " + str(_err.code))
print(_err)
except urllib.error.URLError as _err:
print("HTTPError: " + _err.reason)
print(_err)
環境変数
キー | 値 |
---|---|
REGION | "us-east-1"(固定) |
SLACK_URL | IncommingWebhookのWebhook Url(ドメイン以下) |
TZ | "Asia/Tokyo" |
メイン処理
- getBillingMetric:請求情報の取得
- sendNoticeToLine:LINEに請求情報を送信
- sendNoticeToSlack:Slackに請求情報を送信
※ testable にするため、それぞれの関数を別に分けている
[L8−L16]_index.py
#メイン処理
def lambda_handler(event, context):
#請求情報取得
_billing = getBillingMetric()
_billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ!"
#LINEに送信
sendNoticeToLine(_billing)
#Slackに送信
sendNoticeToSlack(_billing)
請求情報の取得
- boto3でCloudWatch用のClientを作成
- [ハマりポイント] Regionは us-east-1 で固定(Lambdaの共通関数で指定)
- get_metric_statistics関数で請求情報を取得
- 請求情報を呼び出し元に返却
- 現在日-1 から 現在日までの最大の値(=1日は昨月〆、2日以降は当日までの請求額となる)
[L18−L30]_index.py
def getBillingMetric():
_cw = boto3.client('cloudwatch', region_name=_env["REGION"])
_billing = _cw.get_metric_statistics(
Namespace='AWS/Billing',
MetricName='EstimatedCharges',
Dimensions=[{'Name': 'Currency', 'Value': 'USD'}],
StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
EndTime=datetime.datetime.today(),
Period=86400,
Statistics=['Maximum'])
print(_billing)
return _billing
LambdaにIAMロールを設定する
- 一旦、ここまでで請求情報が取得できるかを確認するため、Lambdaに実行ロールを設定する
- [ハマりポイント] 請求情報を参照する権限ではなく、CloudWatchのメトリクスを参照する権限が必要
- 上記URLの例のまま、以下をインラインポリシーに追加したロールを指定
{
"Version": "2012-10-17",
"Statement":[{
"Effect":"Allow",
"Action":["cloudwatch:GetMetricStatistics","cloudwatch:ListMetrics"],
"Resource":"*",
"Condition":{
"Bool":{
"aws:SecureTransport":"true"
}
}
}
]
}
リクエスト送信用の共通関数
- LINEのPUSH API、SlackのIncomming Webhooksを呼び出すときに使うリクエスト送信処理を共通化する
- 関数は urllib.request.Request を使う
- 第一引数には送信先URLを指定
- 第二引数には送信データを指定
- 第三引数には送信ヘッダを指定
[L57−L68]_index.py
def sendRequest(_url, _data, _header):
_req = urllib.request.Request(_url, _data, _header)
try:
with urllib.request.urlopen(_req) as _res:
_body = _res.read()
print(_body)
except urllib.error.HTTPError as _err:
print("HTTPError: " + str(_err.code))
print(_err)
except urllib.error.URLError as _err:
print("HTTPError: " + _err.reason)
print(_err)
LINEに請求情報を送信
- APIのURLはv2のもの
-
[ハマりポイント] POSTデータに送信データを設定する
- toにLINEのユーザIDを文字列形式で指定する(Lambdaの環境変数 で設定)
- messagesにメッセージを配列形式で指定する
- Authorizationヘッダに "Bearer " + LINEのアクセストークンを指定する(Lambdaの環境変数 で設定)
- リクエスト送信処理を呼び出して終了
[L32−L44]_index.py
def sendNoticeToLine(_bill):
_url = 'https://api.line.me/v2/bot/message/push'
_data = json.dumps({
"to": _env["LINE_USER_ID"]
,"messages": [{"type": "text", "text": _bill["message"]}]
}).encode()
_header = {
"Content-type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
}
print(_data)
sendRequest(_url, _data, _header)
Slackに請求情報を送信
- URLはIncomming Webhooks の Webhook URL を指定(Lambdaの環境変数 で設定)
- POSTデータとヘッダはシンプルなもの
- リクエスト送信処理を呼び出して終了
[L46−L55]_index.py
def sendNoticeToSlack(_bill):
_url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
_data = json.dumps({
"text": _bill["message"]
}).encode()
_header = {
"Content-type": "application/json"
}
print(_data)
sendRequest(_url, _data, _header)
Lambda関数をテスト実行し、正常に動作することを確認する
- エラーが出ていたら、エラーメッセージ、エラーコードから原因を切り分けてシューティング
- IAM権限周り
- LINEmessagingAPI
- Slack Incomming Webhooks
- 私はIAM権限と、LINEの400(リクエスト形式エラー)でハマりました
CloudWatch の CloudWatch Events で通知するLambda関数と通知タイミングを設定
CloudWatch の イベント を選択し、「ルールの作成」を選択する
スケジュールイベントを作成し、Cron式で記述
- [ハマりポイント] CloudWatchで日と曜日を指定するときはどちらかに?をつかう
- 以下のキャプチャは毎日GMT 0時(日本時間では9時)にLambda関数を実行する場合
- Cron式は[分 時 日 月 曜日 年]の6項目を半角空白で区切ったもの(Syntaxに問題がなければ、以後10回のトリガー日が表示される)
次のページでイベントに名前と詳細説明を付けて終了
振り返り
- リクエスト送信処理は大体の外部APIを呼び出すときに個別で指定する項目なので、共通関数化しようと思う
- IAMロールへの理解が足りないので、慣れるまでチャレンジする