AWS請求が月25ドルから54ドルへ|原因はコードのバグだった話と、月14ドル削減までの記録
- 3 時間前
- 読了時間: 7分
自社で運用しているAmazon出品者向けのリサーチ自動化システムで、AWSの請求額が月25ドルから54ドルまで膨らみました。原因を調べたところ、コストの問題ではなくコードのバグでした。DynamoDBの書き込みが前月比15倍、SQSのキューには33万件が滞留。この記事では、原因の特定から修正までの過程を実際の数字とコードとともに記録します。
この記事の要点
・AWSのコスト急増は、料金設定ではなくコードのバグが原因だった
・DynamoDBのGSIクエリで、ソートキーと無関係な値を比較していたため重複チェックが常に機能していなかった
・SQSの可視性タイムアウトが実行時間より短く、処理中のメッセージが再配信されていた
・SP-APIの429エラーに固定5秒スリープで対応していたため、リトライストームが発生していた
・3つを修正し、ピーク比で月約14ドルの削減とエラーのほぼ解消を達成した
1. 発端:請求額が半年で2倍になった
運用しているのは、Amazon出品者のセラーIDを起点にASINを収集し、価格や手数料を調査して結果をスプレッドシートに書き出すシステムです。構成はLambda、DynamoDB、SQS FIFOキュー、開発環境にCloud9という、よくあるサーバーレス構成です。
請求額の推移は以下のとおりでした。
・2025年12月:約25ドル/月
・2026年5月:約54ドル/月
処理件数が倍増したわけではありません。にもかかわらず請求だけが倍になっている。この時点で「使いすぎ」ではなく「何かがおかしい」と判断し、Cost Explorerでサービス別の内訳を見るところから始めました。
コストが不自然に増えたときは、いきなり節約策を探さないことです。まず内訳を見て、どのサービスのどのメトリクスが伸びているかを特定する。原因がバグなら、節約策をいくら積んでも根本解決になりません。
2. 原因その1:DynamoDBの書き込みが前月比15倍
Cost Explorerで見ると、DynamoDBの書き込み(WriteRequestUnits)が突出していました。CloudWatch Logsで処理件数を確認したところ、はっきり差が出ました。
・2月の処理ASIN数:928,648件
・3月の処理ASIN数:14,344,546件
約15倍です。セラーリストもASIN数も、これほど増えてはいません。処理そのものが重複していると考え、重複チェックのコードを読み直しました。
原因:GSIのソートキーに日付を渡していた
同じセラーを短期間で再処理しないよう、DynamoDBのGSIに問い合わせる重複チェックを入れていました。そのクエリがこれです。
# 修正前
response = table.query(
IndexName='DateTargetSellerIdIndex',
KeyConditionExpression=(
Key('date').eq(today_str) &
Key('targetSellerId').between(one_week_ago_str, today_str)
)
)問題は最後の行です。このGSIのソートキーは targetSellerId で、入る値はセラーIDです。そこに日付文字列の範囲を between で渡していました。
セラーIDと日付文字列を比較しているので、条件に合致するレコードは当然ゼロになります。重複チェックは常に「未処理」を返し、同じセラーが毎回フル処理されていました。エラーにならないぶん、半年近く気づけませんでした。
修正:日付ごとにループしてソートキーを正しく使う
設計意図はTTLに合わせた7日分のチェックだったので、日付を1日ずつ回してソートキーにはセラーIDを渡す形に直しました。
# 修正後
for i in range(7):
check_date = (today - timedelta(days=i)).strftime('%Y-%m-%d')
response = table.query(
IndexName='DateTargetSellerIdIndex',
KeyConditionExpression=(
Key('date').eq(check_date) &
Key('targetSellerId').eq(targetSellerId)
),
Limit=1
)
if response['Items']:
return True
return FalseGSIのキー設計を後から変更すると、こういう取り違えが起きます。パーティションキーとソートキーに何が入るのかをコメントで明記しておくだけでも防げたはずです。
3. 原因その2:SQSキューに33万件が滞留
次に見つかったのが、ASIN処理用のFIFOキューです。メッセージ数を確認すると336,664件が溜まっており、消化がほぼ止まっていました。
このキューを消費するLambdaは、SP-APIのレート制限に合わせて予約済み同時実行数を2に絞っていました。処理が遅いのは想定内でしたが、それにしても消化されなさすぎます。
原因:可視性タイムアウトが実行時間より短かった
調べると、SQSの可視性タイムアウトが3分、実際の実行時間が大きいセラーで約4分でした。
処理が終わる前にメッセージが再び可視状態になるため、別の実行が同じメッセージを取得します。受信回数だけが積み上がり、最終的にデッドレターキューへ落ちる。実際、DLQには62件の新規メッセージが溜まっていました。
可視性タイムアウトは「Lambdaの最大実行時間より長く」が原則です。Lambdaのタイムアウト設定だけ見て安心していると、この組み合わせを見落とします。
4. 原因その3:429エラーでリトライストーム
さらにCloudWatchのエラー数が高止まりしていました。SP-APIの呼び出しでレート制限(429)が返ったときの処理を見ると、固定で5秒待って再試行する実装になっていました。
# 修正前:常に5秒待って再試行
except Exception as e:
time.sleep(5)
continueレート制限がかかっている状況で全員が5秒後に一斉に再送すれば、当然また429が返ります。待ち時間が伸びないので、これが延々と繰り返される。典型的なリトライストームです。
修正:指数バックオフに変更
待ち時間を倍々に伸ばし、上限を設ける形に変えました。対象は商品オファー情報の取得と手数料取得の2箇所です。
# 修正後:指数バックオフ(最大10回、2→4→8→…→60秒)
MAX_RETRIES = 10
wait = 2
for attempt in range(MAX_RETRIES):
try:
response = call_sp_api()
return response
except Exception as e:
if attempt == MAX_RETRIES - 1:
raise
time.sleep(wait)
wait = min(wait * 2, 60)デプロイ後、CloudWatchのエラー数はほぼゼロまで下がりました。エラーが安定したことを確認してから、同時実行数を2から3、5へと段階的に引き上げています。
キューの消化状況を見るときは、メッセージ数だけでなく NumberOfMessagesDeleted と ApproximateAgeOfOldestMessage を併せて見ると判断しやすくなります。前者が伸びていれば実際に処理が進んでおり、後者が下がっていれば滞留が解消に向かっています。
5. コスト最適化として実施したこと
バグ修正とは別に、構成面でも見直しを行いました。
DynamoDBをオンデマンドからプロビジョンドへ
処理量がある程度読めるワークロードだったため、オンデマンドからプロビジョンド(Auto Scaling有効)に切り替えました。設定値は最小5、最大30WCU、ターゲット使用率70%です。
GSIにもテーブル本体と同じ値を設定しています。GSIのキャパシティが本体より低いと、そこがボトルネックになってスロットリングが発生するためです。
CloudWatchロググループの保持期間を設定
保持期間が無期限のままになっていたロググループが15個ありました。すべて30日に設定しています。手作業だと面倒なので、boto3でまとめて設定するスクリプトを書きました。
import boto3
logs = boto3.client('logs', region_name='us-east-1')
paginator = logs.get_paginator('describe_log_groups')
for page in paginator.paginate(logGroupNamePrefix='/aws/lambda/'):
for lg in page['logGroups']:
name = lg['logGroupName']
if 'retentionInDays' not in lg:
logs.put_retention_policy(
logGroupName=name,
retentionInDays=30
)
print(f'set retention: {name}')
GSIクエリのキャッシュを追加
重複チェックを7日分ループする構造上、1セラーあたり最大7回のクエリが走ります。1回の実行内で同じセラーを何度もチェックしていたため、実行中だけ保持するキャッシュを追加して読み込み回数を削減しました。
Cloud9の自動停止
こちらは既に30分で設定済みでした。EC2インスタンス費用が毎月かかり続けるので、未設定の方は真っ先に確認することをおすすめします。
GSIそのものの削除も検討しましたが、重複チェックで現役使用中だったため見送りました。使っていないGSIがあれば削除は効果が大きい施策です。コードを grep して参照箇所を確認してから判断してください。
6. 結果
2026年6月時点で、ピーク比およそ14ドル/月の削減となりました。7月は月28〜30ドル程度で推移しています。
金額以上に大きかったのは、CloudWatchのエラーがほぼゼロになったことです。毎日エラー通知を見て見ないふりをする状態から抜け出せました。
なお、VPC関連の費用も調べましたが、NAT Gateway、VPCエンドポイント、Elastic IPはいずれも該当せず、IPv4アドレスの課金が残りの要因と見ています。ここはまだ手をつけていません。
7. 振り返って
今回の3つの原因には共通点があります。どれもエラーを出していませんでした。
1. GSIクエリの誤りは、結果がゼロ件になるだけでエラーにならない
2. 可視性タイムアウト不足は、メッセージが再配信されるだけでエラーにならない
3. 固定5秒スリープは、リトライが続くだけで最終的には成功することもある
システムは動いていました。ただ、無駄に動いていました。そして無駄はコストにしか現れませんでした。
請求額はシステムの健康診断表だと考えると見方が変わります。処理件数が変わっていないのに請求が伸びているなら、それはコストの問題ではなく、どこかに異常があるサインです。月に一度Cost Explorerを開く習慣が、結果的にバグ発見の近道になりました。
まとめ
1. コスト急増を見つけたら、節約策より先に原因を特定する
2. DynamoDBのGSIは、キーに何が入るのかを常に意識する
3. SQSの可視性タイムアウトは、Lambdaの最大実行時間より長く設定する
4. 外部APIのレート制限には、固定待機ではなく指数バックオフで対応する
同じような構成でAWSのコストやエラーにお困りの場合は、ロビンプランニング合同会社までご相談ください。自社システムで実際に踏んだ落とし穴をもとに、実務目線で調査と改善をご支援します。

コメント