やりたいこと
私用のAWSの利用料金を毎日自動でチェックし、メールではなくiPhoneへのプッシュ通知で受け取りたい。
商用のアカウントであれば、メールを見る機会やTeams連携できますが使用の場合メールそんなに見なかったりTeams使っていないので。
通知先には ntfy.sh を採用しました。
- 完全無料(セルフホストも可能)
- アカウント登録不要、トピック名を決めるだけで使える
- LambdaからHTTP POST 1回で通知できる
- iPhone用の公式アプリがあり、トピックを購読するだけでプッシュ通知を受信できる
構成図
処理の流れ
- Amazon EventBridge Scheduler が毎日決まった時刻(JST 9:00)に Lambda を起動する
-
AWS Lambda が AWS Cost Explorer API (
GetCostAndUsage) を呼び出し、料金を取得する - Lambda が取得した料金データをメッセージ文に整形する
- Lambda が ntfy.sh の指定トピックURLに対して HTTPS POST でメッセージを送信する
- iPhone の ntfy アプリ が該当トピックを購読しているため、プッシュ通知として受信する
構築方法
構築の自動化にはAWS公式ツールである AWS SAM(Serverless Application Model) を使用する。
sam build && sam deploy でLambda・EventBridge Scheduler・IAM Roleをまとめてデプロイできる。
環境構築
template.yaml のカスタマイズ
sam initで生成されたHello Worldのテンプレートを、以下の方針で書き換えました。
- API Gatewayのイベント定義を削除
-
EventsにType: ScheduleV2(EventBridge Scheduler)を追加し、毎日決まった時刻にLambdaを起動する -
PoliciesにCost Explorer APIの読み取り権限(ce:GetCostAndUsage)のみを許可するインラインポリシーを追加 - ntfy.shのトピックURLは
Parametersとして定義し、sam deploy実行時に入力する形にした(NoEcho: true)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
cost-notify-app
Daily AWS cost notification to iPhone via ntfy.sh
Parameters:
NtfyTopicUrl:
Type: String
NoEcho: true
Description: >
ntfy.sh topic URL to send cost notifications to
(e.g. https://ntfy.sh/your-secret-topic-name)
Globals:
Function:
Timeout: 30
Resources:
CostNotifyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: cost_notify/
Handler: app.lambda_handler
Runtime: python3.13
Architectures:
- x86_64
Environment:
Variables:
NTFY_TOPIC_URL: !Ref NtfyTopicUrl
Policies:
- Statement:
- Effect: Allow
Action:
- ce:GetCostAndUsage
Resource: "*"
Events:
DailySchedule:
Type: ScheduleV2
Properties:
ScheduleExpression: "cron(0 0 * * ? *)"
Description: "Trigger daily AWS cost check at 9:00 JST (UTC+9)"
Outputs:
CostNotifyFunction:
Description: "Cost Notify Lambda Function ARN"
Value: !GetAtt CostNotifyFunction.Arn
CostNotifyFunctionIamRole:
Description: "Implicit IAM Role created for Cost Notify function"
Value: !GetAtt CostNotifyFunctionRole.Arn
Lambda関数の実装
hello_worldディレクトリをcost_notifyにリネームし、Cost Explorerから「当月累計」「前日分」の料金を取得してntfy.shへ通知する処理に書き換えました。
import datetime
import os
import boto3
import requests
# template.yamlのEnvironment.Variablesで設定した、ntfy.shのトピックURL
# (例: https://ntfy.sh/your-secret-topic-name)
NTFY_TOPIC_URL = os.environ["NTFY_TOPIC_URL"]
def _get_unblended_cost(start, end, granularity):
"""指定期間のAWS利用料金(UnblendedCost)をCost Explorer APIから取得する"""
# Cost Explorer(ce) APIのエンドポイントはus-east-1リージョンにしか存在しないため、
# Lambda自体が東京リージョンにデプロイされていてもここではus-east-1を明示指定する
client = boto3.client("ce", region_name="us-east-1")
response = client.get_cost_and_usage(
TimePeriod={
"Start": start.isoformat(),
# Cost ExplorerのTimePeriod.Endは「その日を含まない」排他的な指定
"End": end.isoformat(),
},
Granularity=granularity,
# UnblendedCost: 割引・クレジット適用前の実費ベースの金額
Metrics=["UnblendedCost"],
)
# ResultsByTimeは指定した期間ごとの配列だが、今回は1期間分のみリクエストしているため先頭要素を取得
total = response["ResultsByTime"][0]["Total"]["UnblendedCost"]
return float(total["Amount"]), total["Unit"]
def get_month_to_date_cost():
"""当月1日〜本日までのAWS利用料金合計を取得する"""
today = datetime.date.today()
start_of_month = today.replace(day=1)
# Endは排他的指定のため、本日分の料金も含めるために翌日の日付を渡す
end_exclusive = today + datetime.timedelta(days=1)
return _get_unblended_cost(start_of_month, end_exclusive, "MONTHLY")
def get_previous_day_cost():
"""前日1日分のAWS利用料金を取得する"""
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
# Start=昨日, End=今日(排他的)とすることで、昨日1日分だけが集計対象になる
return _get_unblended_cost(yesterday, today, "DAILY")
def lambda_handler(event, context):
"""EventBridge Schedulerから毎日呼び出されるエントリーポイント"""
month_amount, month_unit = get_month_to_date_cost()
yesterday_amount, yesterday_unit = get_previous_day_cost()
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
# ntfy.shへ送信する通知本文を作成(日本語はHTTPヘッダーではなくbodyに含めることで文字化けを回避)
message = (
f"{today.strftime('%Y-%m-%d')} 時点\n"
f"前日({yesterday.strftime('%m/%d')})の利用料金: {yesterday_amount:.2f} {yesterday_unit}\n"
f"今月の利用料金合計: {month_amount:.2f} {month_unit}"
)
# ntfy.shのトピックURLに対してメッセージ本文をPOSTするだけで、
# そのトピックを購読しているiPhoneのntfyアプリにプッシュ通知が届く
response = requests.post(
NTFY_TOPIC_URL,
data=message.encode("utf-8"),
timeout=10,
)
# ntfy.sh側がエラーステータスを返した場合は例外を送出し、Lambdaの実行結果を失敗として記録する
response.raise_for_status()
return {
"statusCode": 200,
"body": message,
}
requirements.txt
importしているサードパーティパッケージのみを記載する。boto3はLambdaランタイムに標準搭載されているが、AWS公式が「バージョン不一致を避けるため同梱有無に関わらず明記することを推奨」しているため、バージョンを固定して明記してみました。
boto3==1.43.54
requests==2.34.2
sam build
sam build
ntfy.shのトピック作成・iPhoneアプリでの購読設定
- 推測されにくいランダムな文字列でトピック名を決定(例:
aws-cost-xxxxxxxxxxxx) - iPhoneにntfyアプリをインストールし、決めたトピック名を購読
- curlで直接テスト通知を送信し、iPhoneに届くことを確認
curl -d "テスト通知です" https://ntfy.sh/<決めたトピック名>
利用時の注意点(公式ドキュメントより)
- トピック名は事実上「パスワード」: 認証機能がないため、知っている人は誰でも購読・投稿できる。推測されにくい文字列にすることが必須
- 可用性は「ベストエフォート」: 単一サーバーで動作しておりSLAの保証はない。重要な通知を100%保証するものではない(気になる場合はセルフホストも選択肢)
sam deploy --guided によるデプロイ
sam deploy --guided --profile cost-check
| 質問 | 回答 |
|---|---|
| Stack Name | cost-notify-app |
| AWS Region | ap-northeast-1 |
| Parameter NtfyTopicUrl | 決めたntfy.shのトピックURL |
| Confirm changes before deploy | Y |
| Allow SAM CLI IAM role creation | Y |
| Save arguments to samconfig.toml | Y |
デプロイ成功後、cost-notify-appスタックに以下が作成された。
-
CostNotifyFunction(Lambda関数) -
CostNotifyFunctionRole(IAM Role、ce:GetCostAndUsageのみ許可) -
EventBridge Scheduler(毎日9:00 JSTに起動)
動作確認
翌日、EventBridge Schedulerによる定時自動実行でLambdaが起動し、ntfy.sh経由でiPhoneに実際の利用料金(前日分・当月累計)の通知が届くことを確認できました!

