1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】Lambdaを使った週間レポートを月跨ぎ、年跨ぎで生成する際の注意点

Posted at

はじめに

AWS Cost Explorer APIを使って週間コストレポートをLINEに通知するLambda関数を運用していたところ、月末に突然エラーが発生しました。この記事では、その原因と対処法を共有します。

発生したエラー

2026年2月1日(土曜日)の朝、以下のエラーが発生しました。

[ERROR] ClientError: An error occurred (ValidationException) when calling the GetCostAndUsage operation: end date past the beginning of next month
GetCostAndUsage操作の呼び出し時にエラーが発生しました(ValidationException):終了日が翌月の開始日を過ぎています

エラーの原因

AWS Cost Explorer APIのGetCostAndUsageには、「終了日は次の月の最初を超えてはならない」という制限があり、そこに引っかかっていたようです。

問題のコードは以下の部分でした。

def _week_to_date_range_jst(now: datetime):
    """
    今週(月曜00:00 JST) 〜 明日(00:00 JST)  (Endは排他的)
    """
    this_monday = (now - timedelta(days=now.weekday())).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    tomorrow_0 = (now + timedelta(days=1)).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    return this_monday.date().isoformat(), tomorrow_0.date().isoformat()

具体的な問題シナリオ

2026年2月1日(土曜日)08:03に実行された場合:

  • start: 2026-01-26(今週の月曜日)
  • end: 2026-02-02(明日)← ここ

Cost Explorer APIは、1月のデータを取得する際、終了日を2026-02-01(翌月1日)までしか許可しません。しかし、上記のコードでは2026-02-02を指定してしまうため、エラーになります。
当たり前ですが、未来日を指定するようになってしまっていたのですね。
それだと実績値が取れないのでエラーになってしまいます。

解決策

終了日が翌月1日を超えないようにキャップする処理を追加します。

修正後のコード

from datetime import datetime, timedelta, timezone
from decimal import Decimal

JST = timezone(timedelta(hours=9))

def _week_to_date_range_jst(now: datetime):
    this_monday = (now - timedelta(days=now.weekday())).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    tomorrow_0 = (now + timedelta(days=1)).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    
    # 翌月1日を計算
    year = now.year
    month = now.month
    if month == 12:
        next_month_first = datetime(year + 1, 1, 1, tzinfo=JST)
    else:
        next_month_first = datetime(year, month + 1, 1, tzinfo=JST)
    
    end_date = min(tomorrow_0, next_month_first)
    
    return this_monday.date().isoformat(), end_date.date().isoformat()

動作確認

修正後の動作を確認してみます。

image.png

修正後の動作は以下のように確認できました。

{
  "status": "ok",
  "line_status": 200,
  "line_resp": "{\"sentMessages\":[{\"id\":\"\",\"quoteToken\":\"\"}]}",
  "period_this_week": {
    "start": "2026-02-02",
    "end": "2026-02-06",
    "total": "0.1682506699",
    "currency": "USD"
  },
  "period_prev_week": {
    "start": "2026-01-26",
    "end": "2026-02-02",
    "total": "0.6485226749"
  },
  "forecast_week_end": "0.294438672325"
}

image.png

年跨ぎのケース

12月末の場合も同様に処理されます。
今回は単純に週ごとなので、特別に考慮はしていませんが、年ごとに分けたいなどのケースの場合別途処理が必要になります。

# 2025年12月31日(水曜日)の場合
now = datetime(2025, 12, 31, 10, 0, 0, tzinfo=JST)
start, end = _week_to_date_range_jst(now)
print(f"start: {start}, end: {end}")
# 出力: start: 2025-12-29, end: 2026-01-01

その他の注意点

1. Cost Explorer APIのデータ遅延

Cost Explorerのデータは通常、1日程度の遅延があります。当日分のデータが完全に反映されていない可能性があるため、レポート生成時にはこの点を考慮する必要があります。

2. タイムゾーンの扱い

タイムゾーンに関しては、以前の記事でもちょろっとふれましたが、JSTで運用する場合、UTCとの時差(9時間)を正確に扱うことが重要です。特に日付の境界で実行されるジョブでは、タイムゾーンの指定を明示的に行いましょう。

3. 前週データの取得

前週のデータを取得する処理の場合も、同様の問題が発生する可能性があります。念のため確認しておきます。

def _last_week_range_jst(now: datetime):
    """
    前週(月曜00:00 JST) 〜 今週(月曜00:00 JST) (Endは排他的)
    """
    this_monday = (now - timedelta(days=now.weekday())).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    last_monday = this_monday - timedelta(days=7)
    return last_monday.date().isoformat(), this_monday.date().isoformat()

この関数は終了日が「今週の月曜日」なので、月跨ぎでも問題ありませんが、念のため注意が必要です。

まとめ

  • AWS Cost Explorer APIのGetCostAndUsageは、終了日が翌月1日を超えることを許可しない
  • 週間レポートなど、期間を動的に計算する場合は、月末・年末での境界条件を考慮する必要がある

個人検証でも今回のような境界条件でのテストを今後は忘れずにやりましょう笑
APIの利用例だけでなく、例外や注意点までしっかり把握しておく必要がありすね...

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?