🐡
【AWS × LINE】Cost Explorer APIで月額料金をBot通知するLambda構成
✨ 目的
ユーザーがLINE Botにメッセージを送ると、LambdaがAWS Cost Explorer APIを使用してAWSのコストを取得し、請求書風のメッセージとしてLINEへ返信するシステムの開発。サーバーレス設計によりコスト効率が高く、イベント駆動型で課金最小化を実現しています。
⛏️ 構成概要
- ユーザーがLINE Botにメッセージを送信
- LINEはMessagin APIを通じて、Webhook URLにデータ(JSON)をPOST
- API Gateway経由でLambdaがWebhookリクエストを受信
- Lambda関数がコードを実行し、メッセージ内容を解析
- Cost Explorer APIで当月の合計利用料と上位5サービスの内訳を取得
- Secrets ManagerからLINEトークンを取得
- 請求書形式のメッセージを生成し、LINEトークンを使ってLINEに返信
🛠️ 技術スタック
項目 | 内容 |
---|---|
言語 | Python 3.11 |
使用ライブラリ | boto3, requests, json, datetime, os |
AWSサービス | Lambda, API Gateway, Secrets Manager, Cost Explorer |
メッセージAPI | LINE Messaging API(Webhook, replyToken使用) |
AWSサービス
・AWS Lambda
関数名:NotifyAwsCostHandler
・API Gateway
名前:LineBotWebhookAPI
👷ロールとポリシー
ロール | ポリシー | タイプ |
---|---|---|
NotifyAwsCostLambdaPolicy | AWSLambdaBasicExecutionRole-... | カスタマー管理 |
NotifyAwsCostLambdapolicy | カスタマーインライン |
ポリシー
・AWSLambdaBasicExecutionRole-...
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:358353272915:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:358353272915:log-group:/aws/lambda/NotifyAwsCostHandler:*"
]
}
]
}
・NotifyAwsCostLambdaPolicy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadCostAndUsage",
"Effect": "Allow",
"Action": "ce:GetCostAndUsage",
"Resource": "*"
},
{
"Sid": "AllowReadcredentials",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-northeast-1:358353272915:secret:linebot/credentials-*"
}
]
}
🏠 実装内容
1. Secrets Managerからトークン取得(グローバル初期化)
region = os.environ.get("AWS_REGION", "ap-northeast-1")
secret_name = os.environ.get("SECRET_NAME", "linebot/credentials")
secrets_client = boto3.client('secretsmanager', region_name=region)
secret = secrets_client.get_secret_value(SecretId=secret_name)
LINE_ACCESS_TOKEN = json.loads(secret['SecretString'])['LINE_ACCESS_TOKEN']
2. 請求書風メッセージ作成
# ───────────────────────────────────
# 請求書風メッセージを作成する関数
# ───────────────────────────────────
def build_invoice_message(billing_month: str, total: float, breakdown: list[tuple[str, float]]) -> str:
"""
請求書スタイルでメッセージを組み立て(セパレータ幅は固定)
"""
fixed_width = 13 # 固定幅
sep = "─" * fixed_width
header = [
f"【請求期間】",
f"{billing_month}",
f"",
f"【合計金額】",
f"${total:.2f}"
]
lines = []
for name, amt in breakdown:
short_name = SERVICE_NAME_MAP.get(name, name)
lines.append(f" ・{short_name:<5} : $ {amt:>0.2f}")
parts = [sep]
parts.append("AWS利用料請求書".center(fixed_width))
parts.append(sep)
parts += header
parts.append(sep)
parts.append("【内訳】")
parts += lines
parts.append(sep)
return "\n".join(parts)
# サービス名の略称
SERVICE_NAME_MAP = {
"Amazon Elastic Compute Cloud - Compute": "EC2",
"Amazon Simple Storage Service": "S3",
"Amazon Relational Database Service": "RDS",
"AWS Lambda": "Lambda",
"AWS Cost Explorer": "Cost Explorer",
"Amazon CloudWatch": "CloudWatch",
"Amazon DynamoDB": "DynamoDB",
"Amazon Route 53": "Route53",
"Amazon Virtual Private Cloud": "VPC",
"Amazon API Gateway": "API Gateway",
"Amazon Simple Notification Service": "SNS",
"Amazon Simple Queue Service": "SQS",
"Amazon Kinesis": "Kinesis",
"Amazon CloudFront": "CloudFront",
"Amazon Elastic Block Store": "EBS",
"Amazon Elastic File System": "EFS",
"Amazon Redshift": "Redshift",
"Amazon Elastic Load Balancing": "ELB",
"Amazon Simple Email Service": "SES",
"Amazon Elastic Container Service": "ECS",
"Amazon Elastic Container Registry": "ECR",
"Amazon Elastic Container Service for Kubernetes": "EKS",
"Amazon Simple DB": "SimpleDB",
"Amazon CloudTrail": "CloudTrail",
"Amazon Elastic Kubernetes Service": "EKS",
"AWS Identity and Access Management Access Analyzer": "Access Analyzer",
# 必要に応じてここに追加していく
}
3. 【メインLambdaハンドラー】Webhookリクエスト受信とreplyToken抽出
# ───────────────────────────────────
# メインLambdaハンドラー
# ───────────────────────────────────
def lambda_handler(event, context):
try:
# webhookリクエスト受信とreplytoken抽出
body = json.loads(event["body"])
reply_token = body["events"][0]["replyToken"]
# Cost Explorer API 呼び出し
ce = boto3.client("ce", region_name="us-east-1") # Cost Explorerはus-east-1固定
today = datetime.today()
start_date = today.replace(day=1).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
billing_month = today.strftime("%Y年%m月分")
# 合計コスト取得
total_cost_data = ce.get_cost_and_usage(
TimePeriod={"Start": start_date, "End": end_date},
Granularity="MONTHLY",
Metrics=["BlendedCost"]
)
amount = float(total_cost_data["ResultsByTime"][0]["Total"]["BlendedCost"]["Amount"])
# サービス別内訳取得
service_cost_data = ce.get_cost_and_usage(
TimePeriod={"Start": start_date, "End": end_date},
Granularity="MONTHLY",
Metrics=["BlendedCost"],
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}]
)
services = sorted(
[(g["Keys"][0], float(g["Metrics"]["BlendedCost"]["Amount"])) for g in service_cost_data["ResultsByTime"][0]["Groups"]],
key=lambda x: x[1], reverse=True
)[:5] # 上位5サービスだけ抽出
# メッセージ組み立て
invoice_text = build_invoice_message(billing_month, amount, services)
# LINEへ返信
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"
}
payload = {
"replyToken": reply_token,
"messages": [{"type": "text", "text": invoice_text}]
}
response = requests.post("https://api.line.me/v2/bot/message/reply", headers=headers, json=payload)
# ステータス確認
if response.status_code != 200:
print(f"LINEへの送信失敗: {response.status_code}, {response.text}")
return {"statusCode": 500, "body": "Failed to send message to LINE"}
return {"statusCode": 200}
except Exception as e:
print("エラー発生:", str(e))
return {"statusCode": 500, "body": str(e)}
📄 請求書メッセージ例(LINE返信形式)
LINEのスタイルに「おいしい牛乳」を使用しているため、「乳」がはみでてしまっています。お気になさらずに
今後の課題
・コードの細分化
・違うメッセージによって異なる反応をさせる
✅ 修正履歴まとめ
日付 | 項目 | 内容 |
---|---|---|
2025/04/28 | 初版作成 | コード及び環境の構築、ドキュメント作成 |
2025/04/30 | 請求書関数改善 | セパレーター固定、サービス略称表示の導入 |
2025/04/30 | 内訳抽出処理追加 | GroupBy=SERVICE による上位5件取得を実装 |
2025/04/30 |
us-east-1 明示 |
Cost Explorerはこのリージョン固定のため明示化 |
2025/04/30 | コード整理 | グローバル変数と関数の明確な分離・簡潔化 |
2025/05/02 | ドキュメント編集 | 技術概要を編集 |
2025/05/27 | ドキュメント編集 | AWSサービスとロール、ポリシー記載 |
Discussion