はじめに
個人開発で動かしている小規模な Web サービスで、ある月の AWS 請求がいつもの 3 倍になっていた。普段は月数千円程度のサービスなのに、その月だけ請求が跳ねていて、原因を追ったら CloudWatch Logs だった、という話を書く。
21 歳 / エンジニア歴 2 年目、業務では別件をやっていて、個人開発側の請求は完全に油断していた。「インフラ費そんなに伸びるはずないだろ」と思っていたが、ログまわりは想像以上に簡単に膨らむ。
何が起きていたか
サービスは Lambda + API Gateway + DynamoDB + S3 のよくある構成。負荷はそこまで高くなく、月のリクエスト数は数十万程度だった。
請求の内訳を Cost Explorer で見て、明らかに伸びていたのが次の 3 項目だった。
- CloudWatch Logs Data Ingestion (取り込み): 想定の 4 倍以上
- CloudWatch Logs Data Storage (保存): じわじわ増加
- CloudWatch Metrics (カスタムメトリクス): Lambda 内で
put_metric_dataを呼びすぎていた
合計すると月数千円のサービスで CloudWatch 関連だけで数万円。完全に主役交代していた。
原因の特定方法
最初に Cost Explorer のフィルタを Service: CloudWatch に絞り、Group by: Usage Type で見る。これで「取り込み量が多いのか保存量が多いのか」が一発で分かる。
# CLI 派ならこれで日次の取り込み量が見える
aws ce get-cost-and-usage \
--time-period Start=2026-04-01,End=2026-05-01 \
--granularity DAILY \
--metrics "UnblendedCost" \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["AmazonCloudWatch"]}}' \
--group-by Type=DIMENSION,Key=USAGE_TYPE
DataProcessing-Bytes が突出していたので、次に取り込み量の多いロググループを特定する。
aws logs describe-log-groups \
--query 'logGroups[].[logGroupName,storedBytes,retentionInDays]' \
--output table
retentionInDays が null (= 無期限) のロググループが大量にあり、しかも storedBytes が GB 単位のものがいくつかあった。これは「無期限保存 + DEBUG ログ垂れ流し」のサインである。
防御設定5選
1. リテンション設定を全ロググループに強制する
CloudWatch Logs はデフォルトで「無期限保存」になる。これは個人開発では確実に罠で、ロググループ作成と同時にリテンションを切るべきである。Lambda の自動生成ロググループも対象。
# 既存ロググループ全部に 14 日のリテンションを設定
aws logs describe-log-groups --query 'logGroups[].logGroupName' --output text | \
tr '\t' '\n' | \
while read -r LG; do
aws logs put-retention-policy --log-group-name "$LG" --retention-in-days 14
done
CDK / Terraform を使っているなら、リテンション無指定を lint で禁止するのが本筋。
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/myfunc"
retention_in_days = 14 # 必須にする
}
2. ログレベルでサンプリング
DEBUG ログを本番で出しっぱなしにしていたのが最大の戦犯だった。Lambda の場合、POWERTOOLS_LOGGER_SAMPLE_RATE でサンプリングできる。
from aws_lambda_powertools import Logger
logger = Logger(service="myapp") # 環境変数 POWERTOOLS_LOGGER_SAMPLE_RATE=0.1 で 10% サンプリング
def handler(event, context):
logger.debug("incoming event", extra={"event": event})
# ... 通常処理
10% に絞るだけで取り込み量が桁で減る。
3. ロググループの分離
「全部 /aws/lambda/myfunc に流す」をやめて、リクエスト系・バッチ系・エラー系で分ける。リテンションをロググループ単位で変えられるので、エラーは長期保存、デバッグは短期、と運用しやすくなる。
import logging
req_logger = logging.getLogger("req")
err_logger = logging.getLogger("err")
# ハンドラを別ロググループ向けに分ける (CloudWatchLogsHandler 等)
4. Metric Filter の乱用を避ける
Metric Filter は便利だが、1 ロググループに対して大量に貼ると「カスタムメトリクスの数」で課金が跳ねる。PutMetricData を Lambda 内で都度呼ぶ実装も同じ罠を踏む。
EMF (Embedded Metric Format) で 1 回のログ出力に複数メトリクスを乗せる方が圧倒的に安い。
import json
print(json.dumps({
"_aws": {
"Timestamp": 1715000000000,
"CloudWatchMetrics": [{
"Namespace": "MyApp",
"Dimensions": [["Service"]],
"Metrics": [{"Name": "Latency", "Unit": "Milliseconds"}]
}]
},
"Service": "checkout",
"Latency": 123
}))
5. AWS Budgets でアラートを必ず設定
最後は「気づける状態にする」こと。月予算と日次の伸び率に対してアラートを貼る。
aws budgets create-budget --account-id 123456789012 --budget '{
"BudgetName": "monthly-cloudwatch",
"BudgetLimit": {"Amount": "5", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
"CostFilters": {"Service": ["AmazonCloudWatch"]}
}' --notifications-with-subscribers '[{
"Notification": {"NotificationType":"ACTUAL","ComparisonOperator":"GREATER_THAN","Threshold":80},
"Subscribers":[{"SubscriptionType":"EMAIL","Address":"me@example.com"}]
}]'
CloudWatch 単独で budget を切っておけば、今回のような事故は数日で気づける。
まとめ
CloudWatch Logs は「とりあえず出しておく」で済ませると、簡単に AWS 請求の主役になる。個人開発レベルでも月数万円跳ねるので、リテンションとサンプリングは最初から入れておきたい。失敗してから気づいたことなので、これから個人で AWS を触る人は最初に Budgets を切ってから始めることをおすすめする。