Claude Media
Claude CodeでSQSのデッドレターキューを設計する — maxReceiveCountの決め方

Claude CodeでSQSのデッドレターキューを設計する — maxReceiveCountの決め方

SQSのmaxReceiveCountと可視性タイムアウトを、ワーカーの最長処理時間から逆算してTerraformに書かせる手順です。参照系のaws sqsコマンドだけをpermissionsで許可し、設定値を実測で照合します。

SQSのデッドレターキュー(DLQ)は、ソースキューで処理に失敗し続けたメッセージの退避先です。設計で決める値は3つあります。ソースキューの可視性タイムアウト、maxReceiveCount、DLQの保持期間です。Claude Codeにはこの3つを勘で埋めさせず、ワーカーの最長処理時間から逆算させます。

ここでは、逆算の考え方、Terraformの書き方、Claude Codeに渡す権限、設定値を実測で照合する手順を順に示します。DynamoDBのキー設計はDynamoDBテーブル設計の記事が扱っています。ここはキューの失敗経路に絞ります。

3つの値は、何を根拠に決まるのか

まず、AWSのドキュメントが決めている範囲と既定値を表にします。

設定Terraformの引数範囲と既定値
可視性タイムアウトTerraformの引数visibility_timeout_seconds範囲と既定値0〜43200秒(12時間)、既定30秒
最大受信回数Terraformの引数redrive_policy内のmaxReceiveCount範囲と既定値1〜1,000(コンソールの説明)
メッセージ保持期間Terraformの引数message_retention_seconds範囲と既定値60秒〜1209600秒(14日)、既定345600秒(4日)

visibility_timeoutと略して書きがちですが、Terraformの引数名はvisibility_timeout_secondsです。Claude Codeが短い名前で書いてきても、terraform validateで落ちるので、そこで直せます。

可視性タイムアウトは「最長処理時間」に合わせる

メッセージを受信すると、そのメッセージは可視性タイムアウトの間だけ他のコンシューマーから見えなくなります。時間内に削除されなければ再び見えるようになり、別の受信が起きます。

ドキュメントの推奨は、アプリが通常必要とする最大の処理時間に合わせて始めることです。処理時間が読めないなら2分程度から始めて、必要に応じて延ばします。処理中にタイムアウトを延ばし続けるハートビートも勧められています。

ここで効くのが、最長処理時間を平均ではなく最大側で取ることです。短すぎると処理が終わる前にメッセージが再び見え、重複処理になります。長すぎると、失敗したメッセージの再試行が遅れます。

maxReceiveCountは「失敗に耐える回数」で決まる

maxReceiveCountは、コンシューマーがソースキューから受信できる回数の上限です。ドキュメントは、1のような小さい値だと受信の失敗が1回あっただけでDLQに移る、と説明しています。リトライに十分な値にするのが前提です。

ここで効くのが、削除されない受信は失敗として数えられる点です。ReceiveMessageのリファレンスには、削除も可視性の延長もされないまま可視性タイムアウトが切れた受信は、失敗した受信として数えられるとあります。つまり、ワーカーが落ちた場合も、タイムアウトが短すぎて処理が間に合わなかった場合も、同じ1回に数えられます。可視性タイムアウトが短いと、正常に動いているメッセージまでDLQに近づきます。

逆算の式と、DLQに届くまでの見積もり

次の式は公式の値ではなく、設計を進めるための目安です。

  • 可視性タイムアウト = 最長処理時間 × 余裕係数(例では2倍)
  • DLQに届くまでの待ち時間 ≒ 可視性タイムアウト × maxReceiveCount

最長処理時間を90秒と計測したとします。可視性タイムアウトは180秒、maxReceiveCountを3にすると、毎回タイムアウトで失敗する最悪の場合の待ち時間はおよそ9分です。障害の検知を数分で済ませたいのか、一時的な外部APIの失敗を何度も吸収したいのかで、maxReceiveCountの値は変わります。

数字

例で使う設計値

  • 可視性タイムアウト

    180秒

    最長処理時間 × 2

  • maxReceiveCount

    3

    待ち時間の目安は約9分

  • DLQの保持期間

    14日

    ソースの4日より長くする

最長処理時間90秒を前提にした例

DLQの保持期間は、ソースより長くする

標準キューでは、メッセージの期限はもとの送信時刻(エンキュー時刻)を基準に数えられ、DLQへ移っても変わりません。ソースに1日いたメッセージは、DLQの保持期間が4日なら、DLQでは3日後に消えます。ドキュメントは、DLQの保持期間をソースより長く設定するのがベストプラクティスだと述べています。

FIFOキューは挙動が違い、DLQへ移した時点でエンキュー時刻がリセットされます。

Claude Codeに渡す前提を、CLAUDE.mdに書く

逆算の入力が曖昧だと、Claude Codeは既定の30秒と適当な回数を書いてしまいます。入力は先にプロジェクトのCLAUDE.mdに置きます。

## SQSの設計前提
- ワーカーは Lambda ではなく ECS 上の常駐プロセス
- ordersキューの最長処理時間: 90秒(直近の計測値)
- 外部決済APIの一時失敗は、数分以内の再試行で回復する
- 標準キューを使う(FIFOではない)
- DLQの保持期間は、ソースキューより必ず長くする
- 可視性タイムアウトは「最長処理時間 × 2」を出発点にし、根拠をPR説明に書く

最長処理時間は、ワーカーのログから拾った実測値にします。Claude Codeにログを読ませて最大値を出させてもよいのですが、値の出どころをCLAUDE.mdに残しておくと、数か月後の見直しでも根拠が追えます。

Terraformで3つの値を定義する

次はTerraformのAWSプロバイダーのドキュメントにある書き方に沿った例です。redrive_policyのmaxReceiveCountは、文字列ではなく整数で渡します。

locals {
  worker_max_seconds = 90
  visibility_seconds = local.worker_max_seconds * 2
}
 
resource "aws_sqs_queue" "orders_dlq" {
  name                      = "orders-dlq"
  message_retention_seconds = 1209600 # 14日
}
 
resource "aws_sqs_queue" "orders" {
  name                       = "orders"
  visibility_timeout_seconds = local.visibility_seconds
  message_retention_seconds  = 345600 # 4日
 
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn
    maxReceiveCount     = 3
  })
}
 
resource "aws_sqs_queue_redrive_allow_policy" "orders_dlq" {
  queue_url = aws_sqs_queue.orders_dlq.id
 
  redrive_allow_policy = jsonencode({
    redrivePermission = "byQueue"
    sourceQueueArns   = [aws_sqs_queue.orders.arn]
  })
}

最後のredrive_allow_policyは、DLQを使えるソースキューを絞ります。既定ではすべてのソースキューがDLQを使えます。byQueueで指定できるのは最大10本で、denyAllにするとそのキューはDLQとして使えません。

DLQはソースキューと同じアカウント、同じリージョンに作る必要があります。DLQは自動では作られないので、先にキューを作っておきます。種類も揃える必要があり、FIFOキューのDLQはFIFO、標準キューのDLQは標準です。

Claude Codeには、このHCLをそのまま書かせるのではなく、上のCLAUDE.mdを読ませたうえで「値ごとに根拠を1行のコメントで付ける」ことを求めます。根拠がコメントに残らない値は、レビューで差し戻せます。

権限は、キューの状態を読む範囲だけ許可する

設計値を実測で確かめるには、Claude Codeがaws sqsのコマンドを叩く必要があります。ただし、AWS CLIはビルトインの読み取り専用コマンドの一覧に含まれません。許可ルールを自分で書きます。

{
  "permissions": {
    "allow": [
      "Bash(aws sqs get-queue-url *)",
      "Bash(aws sqs get-queue-attributes *)",
      "Bash(aws sqs list-queues *)",
      "Bash(aws sqs list-dead-letter-source-queues *)",
      "Bash(terraform validate *)",
      "Bash(terraform plan *)"
    ],
    "ask": [
      "Bash(aws sqs receive-message *)"
    ],
    "deny": [
      "Bash(aws sqs send-message *)",
      "Bash(aws sqs purge-queue *)",
      "Bash(aws sqs delete-queue *)",
      "Bash(aws sqs delete-message *)",
      "Bash(aws sqs set-queue-attributes *)",
      "Bash(aws sqs start-message-move-task *)",
      "Bash(terraform apply *)"
    ]
  }
}

receive-messageだけはallowではなくaskに置いています。名前は参照系に見えますが、受信は可視性タイムアウトを起動し、受信回数を増やします。ソースキューに対して実行すると、実際のメッセージがDLQに近づきます。DLQに対する受信は中身の確認として意味があるので、実行のたびに人が対象のキューを見て承認する運用にします。

ルールを書くときは、次の3点に注意します。

  • *の前に半角スペースを入れます。Bash(aws sqs list-queues *)はaws sqs list-queuesそのものにも一致します。
  • パイプや&&でつないだコマンドは、サブコマンドごとに許可が必要です。出力の整形はjqに任せず、AWS CLIの--queryで済ませると、許可ルールが増えません。
  • AWS_PROFILE=dev aws sqs ...のように既知の安全な変数以外を代入すると、allowルールは一致しません。プロファイルは--profile devで渡します。

権限ルールは、Claudeが書いたコマンドの文字列に照合されます。/usr/local/bin/awsの絶対パス呼び出しやsh -c '...'の中身はdenyでも止まりません。確実に守りたいなら、Claude Codeに使わせる認証情報を、SQSの読み取りだけを許すIAMポリシーに絞ります。こうした権限の層分けは、AWSの未使用リソースを洗い出す記事でも、describe系の許可とTerraform側の削除を分ける設計として示しています。

落とし穴

引数を絞った許可ルールは、書き方を変えられるとすり抜ける

Bash(aws sqs receive-message --queue-url <DLQのURL> *)のように引数まで固定したくなりますが、権限設定のページは、引数を絞るBashルールは壊れやすいと警告しています。オプションの順序が変わるだけで一致しなくなります。便利機構として使い、境界はIAMに持たせます。

設定値を実測で照合する

デプロイ後は、Terraformに書いた値と、AWS側に入った値が一致しているかを確かめます。

手順

設計値を確かめる流れ

  1. 1

    plan を読む

    terraform planの差分で、可視性タイムアウトとredrive_policyが意図した値で作られるかを確認します。applyは人が実行します。

  2. 2

    ソースキューの属性を取る

    get-queue-attributesでVisibilityTimeoutとRedrivePolicyを取り、HCLの値と突き合わせます。

  3. 3

    DLQ側の関係を確かめる

    list-dead-letter-source-queuesで、DLQを使うソースキューが意図した1本だけかを確認します。

  4. 4

    失敗経路を人が動かす

    開発環境で、わざと失敗するメッセージを人が送り、DLQに届くまでの時間を測ります。

属性の取得は次のように行います。RedrivePolicyはJSONの文字列として返ります。

aws sqs get-queue-attributes \
  --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/orders \
  --attribute-names VisibilityTimeout RedrivePolicy MessageRetentionPeriod \
  --profile dev

返ってくる形は、APIリファレンスの例に沿うと、Attributesの下にVisibilityTimeoutなどが文字列で並びます。ここで"180"とmaxReceiveCountの3を、HCLの値と照らし合わせます。DLQの滞留件数はApproximateNumberOfMessagesで見られます。

4番目の失敗経路の測定は、メッセージを送る操作を含みます。send-messageをdenyに入れているので、Claude Codeではなく人が実行するか、テスト用のスクリプトをレビューしたうえで走らせます。測定の結果が見積もり(この例では約9分)から大きくずれたら、最長処理時間の計測が足りていません。CLAUDE.mdの値を直し、Terraformを更新します。

設計でつまずきやすい点

  • FIFOキューにDLQを付けると順序が崩れます。ドキュメントは、メッセージや操作の厳密な順序を崩したくない場合、FIFOキューでDLQを使わないよう注意しています。
  • maxReceiveCountが3を超える標準キューでは、3回以上受信されて削除されなかったメッセージが、キューの後ろに回されます。その間、ApproximateAgeOfOldestMessageは、まだこの回数に達していない次のメッセージの経過時間を示します。アラームの閾値を決めるときに効きます。
  • 可視性タイムアウトには、受信から12時間という上限があります。延長しても、この12時間は延びません。それを超える処理はStep Functionsに任せるか、処理を分割します。
  • 標準キューは、可視性タイムアウトの間でも重複配信されることがあります。SQSは「少なくとも1回」の配信なので、ワーカーは同じメッセージを2回受けても副作用が出ない作りにします。LambdaのSQSイベントソースでも同様で、バッチの処理に失敗するとバッチ内の全メッセージが、可視性タイムアウトの後にキューへ戻ります。
  • 同時に処理中のメッセージ数にも上限があります。標準キューでは約12万件で、超えるとOverLimitエラーが返ります(ロングポーリングでは新しいメッセージが返らなくなるだけです)。

LambdaでSQSを扱う構成そのものは、aws-serverlessプラグインの記事に手順があります。

まとめ

SQSのDLQ設計は、ワーカーの最長処理時間を起点にすれば、可視性タイムアウトとmaxReceiveCountが連動して決まります。Claude Codeには、入力となる計測値をCLAUDE.mdで渡し、値ごとの根拠をコメントに残させるのが効きます。

権限では、参照系のget-queue-attributesだけを許可し、receive-messageは受信回数を動かすため確認つきにします。書き込み系のコマンドはdenyにして、最後の防壁はIAMに持たせます。設定はapply後に実測で照合し、見積もりとのずれが出たら、直すのはTerraformではなくCLAUDE.mdの入力値です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-le…
  3. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibil…
  4. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configu…
  5. docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessa…
  6. registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue
この記事を共有:XはてブLinkedIn