Article

⏰ EventBridge Schedulerの失敗を取りこぼさない:Retry PolicyとDLQを実務目線で整理する

aws, eventbridge, sqs, lambda, java

定期バッチを Amazon EventBridge Scheduler から Lambda や ECS に投げていると、最初は「指定時刻に起動できればOK」と考えがちです。

しかし本番運用では、次の問題を考える必要があります。

  • 一時的な障害でターゲットを呼び出せなかったらどうするか
  • 何回まで再試行するか
  • いつまで再試行するか
  • 最終的に失敗したイベントをどう残すか
  • 再試行によって同じ処理が複数回実行されても大丈夫か

この記事では、EventBridge Scheduler の Retry Policy(再試行ポリシー)DLQ(Dead-Letter Queue) を中心に、実務での設計ポイントを整理します。

まず結論

本番の定期処理では、基本的に次の構成から検討すると分かりやすいです。

EventBridge Scheduler
        |
        v
      Lambda
        |
     成功 / 失敗
        ^
        |
   Retry Policy
        |
再試行しても失敗
        |
        v
    SQSのDLQ
        |
        v
CloudWatch Alarmなどで検知

重要なのは、Retry と DLQ は役割が違うという点です。

  • Retry Policy: 一時的な失敗から自動復旧する
  • DLQ: 最終的に処理できなかったイベントを残す

片方だけではなく、組み合わせて考えるのが基本です。

EventBridge Schedulerとは

EventBridge Scheduler は、AWS上で日時やcron式を指定して処理を起動できるサービスです。

例えば次のような用途があります。

  • 毎日3時にLambdaを実行する
  • 毎月1日に請求処理を開始する
  • 指定日時にSQSへメッセージを送る
  • ECSタスクを定期起動する

従来の EventBridge の scheduled rule と似ていますが、Scheduler はスケジュール実行に特化したサービスです。

Retry Policyとは

Retry Policy は、Scheduler がターゲットの呼び出しに失敗したときに再試行するための設定です。

主に2つの値を設定します。

MaximumEventAgeInSeconds
MaximumRetryAttempts

MaximumRetryAttempts

最大再試行回数です。

EventBridge Scheduler APIでは 0 から 185 の範囲で指定できます。

例えば3回まで再試行したい場合は次のイメージです。

{
  "MaximumRetryAttempts": 3
}

MaximumEventAgeInSeconds

失敗したイベントを、どれだけの時間再試行対象として保持するかを指定します。

API上の指定範囲は60秒から86400秒、つまり最大24時間です。

{
  "MaximumEventAgeInSeconds": 3600
}

この例では、イベント発生から1時間を超えると、それ以上の再試行を行いません。

どちらが優先される?

再試行は、次のどちらかに到達すると終了します。

MaximumRetryAttempts に到達
             OR
MaximumEventAgeInSeconds に到達

例えば、

MaximumRetryAttempts = 10
MaximumEventAgeInSeconds = 300

と設定していても、5分のイベント有効期間を先に使い切れば10回まで到達しない場合があります。

Scheduler の再試行では exponential backoff(指数バックオフ)が使われます。

指数バックオフとは、失敗するたびに再試行までの間隔を徐々に広げる方式です。障害中のサービスへ短時間に大量のリクエストを送り続けることを避けられます。

DLQとは

DLQ(Dead-Letter Queue)は、最終的に配信できなかったイベントを保存するためのキューです。

EventBridge Scheduler では Amazon SQS の Standard Queue をDLQとして利用できます。

FIFO Queue は Scheduler のDLQには利用できません。

例えば、

Scheduler
   |
   v
Lambda
   |
失敗
   |
Retry
   |
まだ失敗
   |
   v
SQS DLQ

という流れになります。

DLQを設定していれば、再試行を使い切ったイベントを後から調査できます。

AWS CLIで設定する

例として、毎日決まった時刻にLambdaを実行するスケジュールを考えます。

aws scheduler create-schedule \
  --name daily-job \
  --schedule-expression "cron(0 3 * * ? *)" \
  --flexible-time-window '{"Mode":"OFF"}' \
  --target '{
    "Arn":"arn:aws:lambda:ap-northeast-1:123456789012:function:daily-job",
    "RoleArn":"arn:aws:iam::123456789012:role/scheduler-role",
    "RetryPolicy":{
      "MaximumEventAgeInSeconds":3600,
      "MaximumRetryAttempts":3
    },
    "DeadLetterConfig":{
      "Arn":"arn:aws:sqs:ap-northeast-1:123456789012:scheduler-dlq"
    }
  }'

ポイントを分解します。

--schedule-expression

--schedule-expression "cron(0 3 * * ? *)"

実行スケジュールです。

タイムゾーンを別途指定しない場合の扱いには注意してください。日本時間基準で運用したい場合は、スケジュールのタイムゾーン設定も明示的に確認します。

--flexible-time-window

--flexible-time-window '{"Mode":"OFF"}'

Flexible Time Window は、指定時刻の一定範囲内でSchedulerに実行タイミングを分散させる機能です。

厳密な時刻に起動したい処理では OFF を使うケースがあります。

RetryPolicy

"RetryPolicy": {
  "MaximumEventAgeInSeconds": 3600,
  "MaximumRetryAttempts": 3
}

この例では、最大1時間の範囲で3回まで再試行します。

DeadLetterConfig

"DeadLetterConfig": {
  "Arn": "arn:aws:sqs:ap-northeast-1:123456789012:scheduler-dlq"
}

最終的に配信できなかったイベントの保存先です。

DLQを設定しただけでは終わりではない

ここは実務で重要です。

DLQにイベントが入っても、誰も気づかなければ障害を先送りしただけです。

例えば次のようにします。

SQS DLQ
  |
ApproximateNumberOfMessagesVisible
  |
CloudWatch Alarm
  |
SNS / Slackなど

「DLQに1件以上メッセージが入ったら通知する」という監視を用意すると、失敗を検知できます。

Retry回数は多ければいいわけではない

最大185回設定できるからといって、常に185回にする必要はありません。

例えば「毎日3時に集計して6時までに結果が必要」という処理で、24時間再試行しても業務上意味がないかもしれません。

設計時には、

この処理はいつまでに成功すれば意味があるか?

を先に考えると決めやすくなります。

例えば、

MaximumEventAgeInSeconds = 3600
MaximumRetryAttempts = 3

のように、業務上許容できる時間から逆算します。

再試行を入れるなら冪等性も考える

Retry Policyを設定すると、同じ業務処理が再度呼び出される可能性を考える必要があります。

ここで重要になるのが 冪等性(べきとうせい) です。

冪等性とは、同じ処理を複数回実行しても結果が壊れない性質です。

例えば次の実装は危険です。

public void createInvoice(String customerId) {
    invoiceRepository.insert(customerId);
}

1回目でDB登録に成功した直後、レスポンス前に一時的な問題が起きると、再試行で2件目の請求書を作る可能性があります。

そこで、処理単位を一意に識別できるIDを使います。

public void createInvoice(String jobId, String customerId) {
    if (invoiceRepository.existsByJobId(jobId)) {
        return;
    }

    invoiceRepository.insert(jobId, customerId);
}

DB側にも一意制約を設定すると、アプリケーション側の確認だけに依存するより安全です。

ALTER TABLE invoices
ADD CONSTRAINT uk_invoices_job_id UNIQUE (job_id);

Retryを設計するときは、

再試行設定
+
冪等性

をセットで考えるのが重要です。

「ジョブ内部の失敗」と「Schedulerの配信失敗」は分けて考える

もう1つハマりやすいポイントがあります。

SchedulerがLambdaを正常に呼び出せたことと、Lambda内の業務処理が期待どおり完了したことは、設計上分けて考える必要があります。

例えばLambdaがエラーを握りつぶして正常終了すると、呼び出し元からは成功したように見えます。

public void handleRequest() {
    try {
        executeJob();
    } catch (Exception e) {
        log.error("job failed", e);
        // そのまま正常終了
    }
}

障害時の責任範囲を決め、必要なら例外を伝播させる、業務エラーを別のキューへ送る、処理状態をDBへ記録するなどの仕組みを用意します。

実務でのチェックリスト

EventBridge Schedulerで定期処理を作るときは、最低限次を確認しています。

[ ] Retry Policyを設定したか
[ ] 再試行期間は業務上意味のある長さか
[ ] DLQを設定したか
[ ] DLQを監視しているか
[ ] Schedulerの実行ロールに必要な権限があるか
[ ] DLQへ送信するための権限があるか
[ ] 再試行されても処理が壊れないか
[ ] 同じデータを二重登録しないか
[ ] ターゲット内部の失敗を握りつぶしていないか

まとめ

EventBridge Schedulerを使った定期処理では、「起動できること」だけでなく「失敗したときにどうなるか」まで設計しておくことが重要です。

役割を整理すると、

Retry Policy
  -> 一時障害から自動復旧する

DLQ
  -> 復旧できなかったイベントを残す

CloudWatch Alarm
  -> DLQへの流入を検知する

冪等性
  -> 再試行されてもデータを壊さない

となります。

特に本番バッチでは、Retryだけ設定して安心するのではなく、DLQ・監視・冪等性までを1セットとして設計すると、障害時の調査と復旧がかなり楽になります。

参考資料