Contents
はじめに
CloudWatchで請求アラートを設定する事はできますが、超心配性な自分としては、月初~前日までの請求額を毎日メールで確認しておきたい。 AWS Lambdaとwebhookを使ってSlackのチャンネルに通知する方法は多く見られましたが、メールで通知する方法は意外と多くなかったので、まとめてみました。
概要
- 請求額はCost Explorerから取得する。 ※CloudWatchから取得する方法もありますが、双方に差異があり正しい値はCost Explorerであるという情報があったため、Cost Explorerを使う事にしました。
- Lambda関数のランタイムはPython3.7とし、Python 向けのAWS SDK(Boto3)を利用する。
- トリガを設定したLambda関数にて、月初~前日までの合計請求額とサービス毎の請求額を取得し、その内容を整形し、メッセージとしてAmazon SNSのトピックに発行する。
- メッセージを受け取ったSNSトピックは、紐づけたエンドポイント(メールアドレス)宛にメッセージを送信する。
用語の理解
特にAmazon SNSに登場する用語、そしてそれぞれの関係性がややこしかったので、超ざっくりとまとめます。
- ARN:AWSリソースを一意に識別する名前。
- トピック:複数のエンドポイント(ここではメールアドレス)をグループにまとめる機能。
- エンドポイント:配信先。今回はメールアドレスとなります。
- サブスクリプション:トピックとエンドポイントを紐づける
より深く理解するために、以下の記事の用語説明の箇所がとても参考になりましたので、事前に熟読しておく事をお勧めします。
Amazon SNSでプッシュ通知を送るための基礎知識 | UNITRUST
設定方法
Cost Explorerの有効化
Cost Explorerを有効化していない場合は、マイ請求ダッシュボードから有効化します。
SNS トピックの作成
Amazon SNSのサービス画面に移動します。 ※利用できるリージョンは限られています。(サポートされているリージョンおよび国 – Amazon Simple Notification Service) ※今回は、東京リージョン(ap-northeast-1)で設定を進めます。
「トピック」メニューから、「トピックの作成」を押下。
「タイプ」は、「スタンダード」を選択。 「名前」と「表示名」を入力し、「トピックの作成」を押下。 ※ここで設定した「表示名」が、メールの送信者名となります。 ※ちなみに、管理者向けに何か通知するためのトピックとして、今後別の目的での配信にも利用する事を想定し、名前は「sendMailAdmin」、表示名は「管理者通知メール」と汎用的なものしておきました。
サブスクリプション作成
サブスクリプション(+エンドポイント)を作成します。
「サブスクリプションの作成」を押下。 ※画面に表示されているARNは控えておいてください。
下記項目を入力し、「サブスクリプションの作成」を押下。 ※「トピックARN」は自動入力されるはずですが、されていなければ控えておいたトピックARNを入力してください。
項目名 | 入力値・選択値 |
---|---|
トピックARN | 控えておいたトピックのARN |
プロトコル | Eメール |
エンドポイント | 受信メールアドレス |
サブスクリプションの承認
エンドポイントに指定したメールアドレス宛に、「AWS Notification – Subscription Confirmation」という件名で確認メールが送られてくるので、「Confirm subscription」を押下。
トピックに紐づけたサブスクリプションのステータスが「確認済み」となります。
Lambda関数の作成
トピック・サブスクリプション・エンドポイントの作成が完了しました。
トピックのARNに対してメッセージを発行すると、このトピックに紐づいたエンドポイント(メールアドレス)宛にメッセージが配信されるという流れになります。 そのため次に、トピックのARNに対して発行するメッセージを生成するLambda関数を作成します。 まずは、AWS Lambdaのダッシュボードから、「関数の作成」を押下。
オプションが「一から作成」になっている事を確認し、「基本的な情報」に以下を入力し、「関数の作成」を押下。 ※「アクセス権限」の「実行ロールの選択または作成」をクリックし、「AWS ポリシーテンプレートから新しいロールを作成」を選択しておいてください。
項目名 | 入力値・選択値 |
---|---|
関数名 | sendCost(好きな名前で) |
ランタイム | Python 3.7 |
ロール名 | SNSServiceRoleForLambda(好きな名前で) |
ポリシーテンプレート | Amazon SNS 発行ポリシー |
Lambda関数のテスト
次に、請求情報を取得するコードを書いていく事になりますが、ここまでの設定確認のため、まずはテストメッセージを発行する処理を書いてみます。 関数作成後の下部にある「関数コード」欄に、以下のコードを入力します。 ※TopicArn
には、SNSトピック作成時に控えておいたARNを設定します。
import boto3
def lambda_handler(event, context):
sns = boto3.client('sns')
subject = 'Lambdaからのテストメール件名です。'
message = 'Lambdaからのテストメール本文です。'
response = sns.publish(
TopicArn = 'arn:aws:sns:*:*:*',
Subject = subject,
Message = message
)
return response
そして、実際にはトリガーで定期的に実行する事になりますが、手動で送信してみます。
右上の「デプロイ」を押下した後、「テスト」を押下し、「イベント名」に適当な名前を入れ、「作成」を押下。 その他は初期値のままでOK。
元の画面に戻り、再度右上の「テスト」をクリックすると関数が実行され、指定した受信メールアドレスにメールが届いているはずです。
届かない場合は、コード入力欄の下部のコンソール(Execution results)にエラーメッセージが表示されていないか、入力したARNに間違いがないか等確認してください。
請求情報通知用のコード作成
いよいよ、Cost Explorerから請求額を取得し、Amazon SNSで通知するコードを書いていきます。 先ほど作成した関数の「関数コード」欄内を、以下のコードに置き換えます。 TopicArn
には、前回同様SNSトピック作成時に控えておいたARNを設定します。
※後述しますが、追加設定を行わないとテストしてもエラーとなります! ※コードは、Developers.IOの記事のものをベースとさせていただきました。
import boto3 from datetime
import datetime, timedelta, date
def lambda_handler(event, context):
ce = boto3.client('ce')
sns = boto3.client('sns')
## 今月の合計請求額を取得
total_billing = get_total_billing(ce)
## 今月の合計請求額を取得(サービス毎)
service_billings = get_service_billings(ce)
## Amazon SNSトピックに発行するメッセージを生成
(subject, message) = get_message(total_billing, service_billings)
response = sns.publish(
TopicArn = 'arn:aws:sns:*:*:*',
Subject = subject,
Message = message
)
return response
def get_total_billing(ce):
(start_date, end_date) = get_total_cost_date_range()
response = ce.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='MONTHLY',
Metrics=[
'AmortizedCost'
]
)
return {
'start': response['ResultsByTime'][0]['TimePeriod']['Start'],
'end': response['ResultsByTime'][0]['TimePeriod']['End'],
'billing': response['ResultsByTime'][0]['Total']['AmortizedCost']['Amount'],
}
def get_service_billings(ce):
(start_date, end_date) = get_total_cost_date_range()
response = ce.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='MONTHLY',
Metrics=[
'AmortizedCost'
],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'SERVICE'
}
]
)
billings = []
for item in response['ResultsByTime'][0]['Groups']:
billings.append({
'service_name': item['Keys'][0],
'billing': item['Metrics']['AmortizedCost']['Amount']
})
return billings
def get_total_cost_date_range():
start_date = date.today().replace(day=1).isoformat()
end_date = date.today().isoformat()
## get_cost_and_usage()のstartとendに同じ日付は指定不可のため、今日が1日なら「先月1日から今月1日(今日)」までの範囲にする
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_message(total_billing, service_billings):
start = datetime.strptime(total_billing['start'], '%Y-%m-%d').strftime('%Y/%m/%d')
## Endの日付は結果に含まないため、表示上は前日にしておく
end_today = datetime.strptime(total_billing['end'], '%Y-%m-%d')
end_yesterday = (end_today - timedelta(days=1)).strftime('%Y/%m/%d')
total = round(float(total_billing['billing']), 2)
subject = f'{start}~{end_yesterday}の請求額:${total:.2f}'
message = []
message.append('【内訳】')
for item in service_billings:
service_name = item['service_name']
billing = round(float(item['billing']), 2)
if billing == 0.0:
## 請求無しの場合は内訳を表示しない
continue
message.append(f'・{service_name}: ${billing:.2f}')
return subject, '\n'.join(message)
これで完成かと思いきや、Lamdaに割り当てたロールにCost Explorerへアクセスする権限がないので、以下のようなエラーとなります。
"errorMessage": "An error occurred (AccessDeniedException) when calling the GetCostAndUsage operation: User: arn:aws:sts::251745928455:assumed-role/SNSServiceRoleForLambda/sendCost is not authorized to perform: ce:GetCostAndUsage on resource: arn:aws:ce:us-east-1:251745928455:/GetCostAndUsage"
そこで、IAM管理画面にて、Cost Explorerへアクセス出来るポリシーをロールにアタッチします。 ※関数の作成時に「カスタムロールを作成」を選択し、jsonでポリシーを一気に割り当てる方法もあるようですが、2020/5時点では選択肢にありませんでした。
ポリシーの作成とアタッチ
まず、IAM管理画面のポリシー一覧を表示し、「ポリシーの作成」を押下。
以下の項目を入力し、「ポリシーの確認」を押下。
項目名 | 入力値・選択値 |
---|---|
サービス | Cost Explorer Service |
アクション | 「GetCostAndUsage」と検索しチェックを入れる |
ポリシーの確認画面で、「名前」を入力し、「ポリシーの作成」を押下。 ※ここでは名前を「AmazonCostExplorerGetCostAccess」としました。
ロールの一覧画面に移動し、Lambdaに割り当てたロールを選択。
「ポリシーをアタッチします」を押下。
「ポリシーのフィルタ」で、ポリシー作成の際に設定した名前を入力して検索(この記事の例では「AmazonCostExplorerGetCostAccess」)し、ヒットしたものにチェックを入れ、「ポリシーのアタッチ」を押下。
Lambda関数の実行
これで関数が正常に実行できる状態になったので、作成した関数の設定画面右上の「テスト」を押下します。
全て正しく設定できていれば、以下のようなメールが届くはずです。
※Cost Explorerを有効化したばかりの時は、以下のように「まだデータが無いのでしばらく待ってね」というエラーが発生しますので、数日待ってから再度試してみて下さい。
{
"errorMessage": "An error occurred (DataUnavailableException) when calling the GetCostAndUsage operation: Data is not available. Please try to adjust the time period. If just enabled Cost Explorer, data might not be ingested yet",
"errorType": "DataUnavailableException",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 9, in lambda_handler\n total_billing = get_total_billing(ce)\n",
" File \"/var/task/lambda_function.py\", line 34, in get_total_billing\n 'AmortizedCost'\n",
" File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n return self._make_api_call(operation_name, kwargs)\n",
" File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"
]
}
トリガーの設定
最後に、毎日決まった時間にメール通知するためのトリガーを設定します。 関数の設定画面の左側「トリガーを追加」を押下。
以下のように設定し、「追加」を押下。
項目名 | 入力値・選択値 |
---|---|
トリガーを選択 | CloudWatch Events/EventBridge |
ルール | 新規ルールの作成 |
ルール名 | sendDailyCost(適当に) |
ルールタイプ | スケジュール式 |
スケジュール式 | cron(0 14 ? * * *) |
トリガーの有効化 | チェックする |
今回は23時に設定しました。 注意点として、時間はUTCで設定するので、JST(日本標準時)から9時間分減算した時刻を設定します。
あとは、毎日指定した時間にメールが届く事を確認してください。
これで、安心して毎日眠れますね!