
検証環境のリソースを削除し忘れて重課金になったので、それを防止するためにKiroでアラームを鳴らしまくる仕組みを Vibe で作ってみた
この記事は アノテーション株式会社 AWS Technical Support Advent Calendar 2025 | Advent Calendar 2025 - Qiita 19 日目の記事です。
はじめに
猫とアポロチョコとSystems Managerが好きな m.hayakawa です。
今年も終わりに近づいております。読者の方はなにかしらやらかしたことは、なかったでしょうか。
私はありました。
というわけで、そのやらかし内容と、それを防止する仕組みを Kiro の Vibe 作ってみたので、ブログ記事にします。転んでもただでは起きないのが大事。
やらかし内容
Amazon FSx for NetApp ONTAP で表示されるCloudWatchメトリクスを確認する目的で数分だけ構築しようとしていました。
しかし、環境の削除を失念しており、気付いたらかなりの額が課金されていました。(具体的な数字は控えますが、グラフで察してください)

再発防止策を考える
検証環境では下記の対策は実施済みの状況です。(AWS Budget が都合上使えないので、下記の仕組みを作っています)
- ある料金しきい値を超えた場合、メールが送付され、そのメールが Slack bot に通知される
しかし、上記の通知は一度のみであり、繰り返し通知されないことから、スルーをしてしまっていました。
私はそそっかしいので、こういったことは何度も何度も通知をさせないと、同じことが起こると思いました(実際2度目)。
Kiroを使ってみる
頭の中には下記の構成が出来上がっていました。
- EventBridge Scheduler で 1 時間に 1 回 Lambda をキックする
- Lambda で一日の料金をチェックする
- 一日の料金が 1 ドルを超えていたら、Amazon SNS で Slack へ通知する
- 上記一連の構築を CloudFormation で実行する
さて、いざこれをイチから構築しようとすると時間がかかりそうだなと思いました。
私はあまり慣れていなかったのですが、時間短縮と勉強のため、Vibe Coding というものとやってみようとなりました。
Kiro の構築はこのブログでは紹介しませんが、下記のドキュメントの「デプロイオプション 1」を元に環境を構築しました。
ステップ 1: デプロイオプションを選択する - Amazon Q Developer
さっそく Vibe してみる
Let's build ということで、下記の内容を Vibe してみます。

検証環境にて毎日の料金チェックを実施したいです。現時点でほとんどの場合一日1ドルに満たない料金ですが、リソースの削除し忘れを防止するため、下記の構成をCloudFormation で構築したいです。
- EventBridge Scheduler で 1 時間に 1 回 Lambda をキックする
- Lambda で一日の料金をチェックする
- 日付指定は実行日の前日から当日
- 一日の料金が 1 ドルを超えていたら、Amazon SNS で Slackメール へ通知する
- 超過を検知するたびに毎回通知する
- Slack メールのメールアドレスは「<SAMPLE>@<サブドメイン>.slack.com>」です。
適したCloudFormationのyamlファイルを作成してください。CloudFormationのyamlファイルには絵文字を含めないでください。必要な情報があれば、質問をしてください。
実際のyamlファイルは要件が確定するまで作成しないでください。
なぜか初回だけ、謎のメッセージを吐いて失敗しました。

その後、新しいセッションを作成して、test と入力した後、再度同内容を入力したら、問題なく実行できました。なんだったんだろう・・・
ということで、気を取り直して再度質問を投げたところ、下記の内容が返ってきました。
検証環境での料金監視システムの構築について理解しました。CloudFormationテンプレートを作成する前に、いくつか確認させてください:
## 確認事項
1. AWS料金取得方法について
- Cost Explorer APIを使用して料金を取得する予定でしょうか?
- それとも他の方法(Billing APIなど)をお考えでしょうか?
2. 通知の詳細について
- Slack通知の内容に含めたい情報はありますか?(料金額、前日比、リソース詳細など)
- 通知のタイトルや形式に希望はありますか?
3. Lambda実行環境について
- Pythonランタイムで問題ないでしょうか?
- 特定のバージョンの指定はありますか?
4. IAM権限について
- Cost Explorer APIへのアクセス権限
- SNSへの発行権限
- CloudWatch Logsへの書き込み権限
- これらの権限設定で問題ないでしょうか?
5. エラーハンドリングについて
- API呼び出し失敗時の動作はどうしますか?
- Lambda実行エラー時の通知は必要でしょうか?
6. 料金の通貨について
- USD建てで問題ないでしょうか?
これらの詳細が確定しましたら、適切なCloudFormationテンプレートを作成いたします。
Credits used: 0.1
Elapsed time: 7s
おおっと、これだけで 0.1 クレジットを使うんですね。検証の場合は使い切ることはなさそうですが、それなりに使用量は考慮したほうがよさそうです。
では、下記の内容を返してみます。
1. Cost Explorer API を使用してください
2. 超過料金額、前日比、超過したリソース詳細を含めてください。タイトルは「料金が超過しました。」にしてください。
3. Pythonの最新バージョンを指定します
4. 提案いただいた権限をすべて付与したIAM権限を作成してください。
5. API呼び出し失敗時にもSlackへ通知してください。
6. USD建てで問題ありません。
下記が返ってきました。
要件を確認いたしました。CloudFormationテンプレートを作成します。
Created cost-monitoring-stack.yaml
CloudFormationテンプレートを作成しました。このテンプレートには以下の機能が含まれています:
主要コンポーネント
EventBridge Scheduler: 1時間に1回Lambdaを実行 Lambda関数: Cost Explorer APIで料金チェック、閾値超過時にSNS通知 SNS: Slackメール通知 IAM Role: 必要な権限を付与
主な機能
前日の料金を取得し、1ドル閾値と比較
超過時の通知内容:
超過料金額
前日比較
サービス別詳細(上位10サービス)
API呼び出し失敗時もエラー通知
Python 3.12使用
デプロイ方法
aws cloudformation create-stack \
--stack-name cost-monitoring \
--template-body file://cost-monitoring-stack.yaml \
--parameters ParameterKey=SlackEmail,ParameterValue=your-email@subdomain.slack.com \
--capabilities CAPABILITY_NAMED_IAM
パラメータでSlackメールアドレスと閾値(デフォルト1.0ドル)を設定できます。SNSサブスクリプション確認メールが届くので、確認してください。
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Daily cost monitoring system with EventBridge Scheduler, Lambda, and SNS notifications'
Parameters:
SlackEmail:
Type: String
Description: 'Slack email address for notifications'
Default: 'sample@subdomain.slack.com'
CostThreshold:
Type: Number
Description: 'Daily cost threshold in USD'
Default: 1.0
Resources:
# SNS Topic for notifications
CostAlertTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: cost-monitoring-alerts
DisplayName: Cost Monitoring Alerts
# SNS Subscription for Slack email
SlackEmailSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref CostAlertTopic
Protocol: email
Endpoint: !Ref SlackEmail
# IAM Role for Lambda function
CostMonitoringLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: cost-monitoring-lambda-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: CostExplorerAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ce:GetCostAndUsage
- ce:GetUsageReport
- ce:GetCostCategories
- ce:GetDimensionValues
- ce:GetReservationCoverage
- ce:GetReservationPurchaseRecommendation
- ce:GetReservationUtilization
- ce:GetUsageReport
Resource: '*'
- PolicyName: SNSPublishAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref CostAlertTopic
# Lambda function for cost monitoring
CostMonitoringFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: cost-monitoring-function
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt CostMonitoringLambdaRole.Arn
Timeout: 300
Environment:
Variables:
SNS_TOPIC_ARN: !Ref CostAlertTopic
COST_THRESHOLD: !Ref CostThreshold
Code:
ZipFile: |
import json
import boto3
import os
from datetime import datetime, timedelta
from decimal import Decimal
def lambda_handler(event, context):
try:
# Initialize clients
ce_client = boto3.client('ce')
sns_client = boto3.client('sns')
# Get environment variables
sns_topic_arn = os.environ['SNS_TOPIC_ARN']
cost_threshold = float(os.environ['COST_THRESHOLD'])
# Calculate date range (yesterday to today)
today = datetime.now().date()
yesterday = today - timedelta(days=1)
# Get cost data for yesterday
yesterday_response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': yesterday.strftime('%Y-%m-%d'),
'End': today.strftime('%Y-%m-%d')
},
Granularity='DAILY',
Metrics=['BlendedCost'],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'SERVICE'
}
]
)
# Get cost data for day before yesterday for comparison
day_before_yesterday = yesterday - timedelta(days=1)
previous_day_response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': day_before_yesterday.strftime('%Y-%m-%d'),
'End': yesterday.strftime('%Y-%m-%d')
},
Granularity='DAILY',
Metrics=['BlendedCost']
)
# Calculate total cost for yesterday
yesterday_total = 0
service_costs = []
if yesterday_response['ResultsByTime']:
for group in yesterday_response['ResultsByTime'][0]['Groups']:
service_name = group['Keys'][0]
cost = float(group['Metrics']['BlendedCost']['Amount'])
yesterday_total += cost
if cost > 0:
service_costs.append({
'service': service_name,
'cost': cost
})
# Calculate previous day total for comparison
previous_day_total = 0
if previous_day_response['ResultsByTime']:
previous_day_total = float(
previous_day_response['ResultsByTime'][0]['Total']['BlendedCost']['Amount']
)
# Check if cost exceeds threshold
if yesterday_total > cost_threshold:
# Calculate difference from previous day
cost_difference = yesterday_total - previous_day_total
difference_sign = '+' if cost_difference >= 0 else ''
# Sort services by cost (descending)
service_costs.sort(key=lambda x: x['cost'], reverse=True)
# Create detailed message
message = f"料金が超過しました。\n\n"
message += f"昨日の料金: ${yesterday_total:.2f} USD\n"
message += f"前日比: {difference_sign}${cost_difference:.2f} USD\n"
message += f"閾値: ${cost_threshold:.2f} USD\n"
message += f"超過額: ${yesterday_total - cost_threshold:.2f} USD\n\n"
message += "サービス別料金詳細:\n"
for service in service_costs[:10]: # Top 10 services
message += f"- {service['service']}: ${service['cost']:.2f} USD\n"
if len(service_costs) > 10:
message += f"... その他 {len(service_costs) - 10} サービス\n"
message += f"\n日付: {yesterday.strftime('%Y-%m-%d')}"
# Send SNS notification
sns_client.publish(
TopicArn=sns_topic_arn,
Subject="料金が超過しました。",
Message=message
)
print(f"Cost alert sent: ${yesterday_total:.2f} USD exceeds threshold ${cost_threshold:.2f} USD")
else:
print(f"Cost within threshold: ${yesterday_total:.2f} USD")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Cost monitoring completed successfully',
'yesterday_cost': yesterday_total,
'threshold': cost_threshold,
'alert_sent': yesterday_total > cost_threshold
})
}
except Exception as e:
error_message = f"料金監視でエラーが発生しました。\n\nエラー詳細: {str(e)}\n日時: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
# Send error notification
try:
sns_client = boto3.client('sns')
sns_topic_arn = os.environ['SNS_TOPIC_ARN']
sns_client.publish(
TopicArn=sns_topic_arn,
Subject="料金監視システムエラー",
Message=error_message
)
except:
pass # If SNS also fails, just log the error
print(f"Error in cost monitoring: {str(e)}")
raise e
# IAM Role for EventBridge Scheduler
SchedulerExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: cost-monitoring-scheduler-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: scheduler.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaInvokeAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt CostMonitoringFunction.Arn
# EventBridge Schedule for hourly execution
CostMonitoringSchedule:
Type: AWS::Scheduler::Schedule
Properties:
Name: cost-monitoring-hourly-schedule
Description: 'Hourly schedule for cost monitoring Lambda function'
FlexibleTimeWindow:
Mode: 'OFF'
ScheduleExpression: 'rate(1 hour)'
State: ENABLED
Target:
Arn: !GetAtt CostMonitoringFunction.Arn
RoleArn: !GetAtt SchedulerExecutionRole.Arn
Outputs:
SNSTopicArn:
Description: 'ARN of the SNS topic for cost alerts'
Value: !Ref CostAlertTopic
Export:
Name: !Sub '${AWS::StackName}-SNSTopicArn'
LambdaFunctionArn:
Description: 'ARN of the cost monitoring Lambda function'
Value: !GetAtt CostMonitoringFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-LambdaFunctionArn'
ScheduleArn:
Description: 'ARN of the EventBridge schedule'
Value: !GetAtt CostMonitoringSchedule.Arn
Export:
Name: !Sub '${AWS::StackName}-ScheduleArn'
なんだかとてもよさそうなので、そのままデプロイしてもらいましょう。
では、下記パラメータで実際にデプロイをしてください。
CostThreshold=1.0
SlackEmail=<SAMPLE>@<サブドメイン>.slack.com>

現在、諸事情で PowerShell で AWS コマンドを実行できないようにしているため、結果的にデプロイに失敗しましたが、失敗後のアフターケアも回答してくれるのは、素直にすごいなと思いました。
では、マネジメントコンソールから検証環境へデプロイしてみます。
CloudFormation のコンソールから、当該の yaml ファイルをアップロードし、パラメータとして Slack の通知先のメールアドレスを入力し、スタックを作成したところ、問題なく作成できました。

Slack に飛んでくる Amazon SNS トピックのサブスクライブも同時に済ませておきましょう。

動作チェック
Lambda 関数: DailyCostChecker の CloudWatch Logs を確認したところ、コストは取れていると見えました。
2025-12-10T13:18:05.551+09:00
INIT_START Runtime Version: python:3.12.v99 Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:42569371fb03ced3fe5b4d63763662bf9b13a5d4299cb453cc55a71374SAMPLE
2025-12-10T13:18:05.859+09:00
START RequestId: bd6938f4-54a9-4851-804f-709a7bSAMPLE Version: $LATEST
2025-12-10T13:18:09.396+09:00
Cost within threshold: $0.82 USD
2025-12-10T13:18:09.399+09:00
END RequestId: bd6938f4-54a9-4851-804f-709a7bSAMPLE
2025-12-10T13:18:09.399+09:00
REPORT RequestId: bd6938f4-54a9-4851-804f-709a7bSAMPLE Duration: 3539.60 ms Billed Duration: 3845 ms Memory Size: 128 MB Max Memory Used: 86 MB Init Duration: 304.81 ms
念のため、AWS CLI での結果も確認してみます(余談:ちょっとしたワンライナーコマンドも生成AIですぐに作れてしまうのはありがたい)。
# 前日と当日のサービス別コスト(上位5つ + 合計)
aws ce get-cost-and-usage \
--time-period Start=$(date -d "yesterday" +%Y-%m-%d),End=$(date -d "tomorrow" +%Y-%m-%d) \
--granularity DAILY \
--metrics BlendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--output json | jq -r '
.ResultsByTime[] |
.TimePeriod.Start as $date |
.Groups |
map(select(.Keys[0] != "Tax")) |
map(.Metrics.BlendedCost.Amount |= tonumber) |
sort_by(.Metrics.BlendedCost.Amount) |
reverse |
(
["Date: " + $date, "", ""],
["", "", ""],
["Rank", "Service", "Cost (USD)"],
["----", "-------", "----------"],
(.[0:5] | to_entries | .[] |
[(.key + 1 | tostring), .value.Keys[0], (.value.Metrics.BlendedCost.Amount | . * 100 | round / 100 | tostring)]
),
["", "", ""],
["", "TOTAL (All Services)", (map(.Metrics.BlendedCost.Amount) | add | . * 100 | round / 100 | tostring)]
) | @tsv
' | column -t -s $'\t'
Date: 2025-12-09
Rank Service Cost (USD)
---- ------- ----------
1 Kiro 0.65
2 AWS Security Hub 0.12
3 AWS Key Management Service 0.02
4 Amazon GuardDuty 0.02
5 Amazon Simple Storage Service 0.01
TOTAL (All Services) 0.82
Date: 2025-12-10
Rank Service Cost (USD)
---- ------- ----------
1 Kiro 0.65
TOTAL (All Services) 0.65
タイムラグがある関係で、コマンド実行日のコストはすべて取れていなさそうですが、前日分のコストは正常に取れていそうです。
それでは、0.3ドルほど課金を増やすために、EC2(c5.4xlarge)を30分ほど起動し、終了してみます。翌朝に通知は来るでしょうか。
翌朝10時以降に Slack 通知がされました!

Lambda の実行ログも超過したメッセージが記録されています。
2025-12-11T10:17:41.068+09:00
Cost alert sent: $1.63 USD exceeds threshold $1.00 USD
よく見たら Cost Explorer の料金も若干増えていますね。EventBridge スケジューラーの実行間隔も調整した方が良さそうです。例えば、午前10時、午前11時、午後16時、午後17時、午後18時などにしてもいいかもしれませんね。
ちょっとした変更も Kiro ならやってくれる。
マネジメントコンソール上でデプロイを行い、問題なく稼働していることを確認しました。
EventBridgeの実行スケジュールを下記のように変更したいです。CloudFormationを新規ファイルで作り直してください。
- 日本時間 10:00
- 日本時間 11:00
- 日本時間 16:00
- 日本時間 17:00
- 日本時間 18:00
元のyamlファイルの差分を返信に記載してほしいです。
指定された時間でのスケジュール実行に変更したCloudFormationテンプレートを作成します。
Created: cost-monitoring-stack-updated.yaml
元のyamlファイルからの主な差分
変更されたセクション
1. EventBridge Scheduleの変更
削除:
# EventBridge Schedule for hourly execution
CostMonitoringSchedule:
Type: AWS::Scheduler::Schedule
Properties:
Name: cost-monitoring-hourly-schedule
Description: 'Hourly schedule for cost monitoring Lambda function'
FlexibleTimeWindow:
Mode: 'OFF'
ScheduleExpression: 'rate(1 hour)'
State: ENABLED
Target:
Arn: !GetAtt CostMonitoringFunction.Arn
RoleArn: !GetAtt SchedulerExecutionRole.Arn
追加:
# 5つの個別スケジュール
CostMonitoringSchedule10AM: # 10:00 JST (01:00 UTC)
CostMonitoringSchedule11AM: # 11:00 JST (02:00 UTC)
CostMonitoringSchedule4PM: # 16:00 JST (07:00 UTC)
CostMonitoringSchedule5PM: # 17:00 JST (08:00 UTC)
CostMonitoringSchedule6PM: # 18:00 JST (09:00 UTC)
2. Outputsセクションの変更
削除:
ScheduleArn:
Description: 'ARN of the EventBridge schedule'
Value: !GetAtt CostMonitoringSchedule.Arn
追加:
ScheduleArns:
Description: 'ARNs of the EventBridge schedules'
Value: !Sub
- '${Schedule1}, ${Schedule2}, ${Schedule3}, ${Schedule4}, ${Schedule5}'
- Schedule1: !GetAtt CostMonitoringSchedule10AM.Arn
# ... 他4つのスケジュール
時間設定について
日本時間(JST)からUTCへの変換:
JST 10:00 → UTC 01:00
JST 11:00 → UTC 02:00
JST 16:00 → UTC 07:00
JST 17:00 → UTC 08:00
JST 18:00 → UTC 09:00
新しいテンプレートで既存スタックを更新するか、新規デプロイしてください。
早速、既存スタックを更新し、EventBridge スケジューラーを確認したところ、5つのスケジュールが登録されていました。

ひとつのスケジュールで cron 式で工夫できそうなのでさらに指示をしました。
良いですね!ですが、EventBridgeスケジュールが5つ作られるのはやや冗長かと思います。ひとつのEventBridgeスケジュールでcron式を上手に使うことは考慮できませんか?
おっしゃる通りです!cron式では複数の時間を一つの式で指定できます。1つのEventBridgeスケジュールで対応できるように修正します。
2 edits to file
cost-monitoring-stack-updated.yaml
完璧です!1つのEventBridgeスケジュールで複数の実行時間を指定するように修正しました。
修正内容
cron式の活用:
ScheduleExpression: 'cron(0 1,2,7,8,9 * * ? *)'
この式は以下の意味です:
0 = 分(0分)
1,2,7,8,9 = 時(UTC時間で1,2,7,8,9時)
* = 日(毎日)
* = 月(毎月)
? = 曜日(指定なし)
* = 年(毎年)
実行時間(日本時間)
10:00 JST (01:00 UTC)
11:00 JST (02:00 UTC)
16:00 JST (07:00 UTC)
17:00 JST (08:00 UTC)
18:00 JST (09:00 UTC)
これで5つのスケジュールリソースが1つに集約され、管理がシンプルになりました。リソース数も削減されてコスト効率も向上します。
EventBridge スケジューラーを確認したところ、1つのスケジュールで集約されていました。

こういった変更もサクッとできるのは Kiro のすごさかなと思います。
まとめ
AWS Cost Explorer API の反映上、即効性のあるアラームとはなりませんが、数日放置するといったことはなくなることが期待できると思います。
やらかしがきっかけではあったものの、ほんの数回やり取りをするだけで、AWS の環境を構築できてしまう Kiro のすごさを感じたことはプラスになったと思いたいです。
今年の禊は今年の内にということで、筆を置きたいと思います。来年こそはやらかしませんように。







