Claude Media
Claude CodeでSQS FIFOのMessageGroupIdと重複排除IDを設計する

Claude CodeでSQS FIFOのMessageGroupIdと重複排除IDを設計する

SQS FIFOの並列度はグループIDの数で決まり、重複排除は5分窓で効きます。Claude Codeに設計を任せ、テストで確かめる流れを示します。

SQS FIFOキューでは、MessageGroupIdが順序の単位と並列度を同時に決め、MessageDeduplicationIdが5分間の重複排除を決めます。この2つの値をClaude Codeに任せるときは、設計の前提を渡したうえで、挙動を確かめるテストを先に書かせる進め方が安全です。この記事では、グループIDの切り方、重複排除IDの選び方、テストの書き方、Claude Codeに持たせる設定の順に説明します。

グループIDは「順序を守る単位」と「並列度」の交換条件

MessageGroupIdが同じメッセージは、1件ずつ、厳密な順序で処理されます。同じグループのメッセージが同時に処理されることはありません。グループが違えば、複数のコンシューマーが並行して読めます。

つまりグループIDは、順序保証の範囲を決める値であり、並列度の上限を決める値でもあります。

くらべる

グループIDの切り方で変わるもの

厳密な全体順序

全メッセージで同じID

すべてが1本の列になり、並列処理はできません。全体で順序が必要なときだけの選択です。

順序と並列の両立

業務上の単位ごとにID

注文ID、ユーザーID、テナントIDなど、順序が必要な範囲だけを揃えます。範囲の外は並列に流れます。

グループ数に上限はありません。グループIDの数は、キューのクォータ表で「No quota」と書かれています。スループットを上げたいときは、グループの数を増やす方向で考えます。高スループットFIFOのページにも、リクエスト容量を増やすにはメッセージグループの数を増やすことが推奨されると書かれています。

受信側の動きを押さえる

受信の挙動は、設計に直接効きます。

  • 1回の受信で、同じグループのメッセージを最大10件までまとめて受け取れる
  • 受け取ったメッセージを削除するか、可視性タイムアウトが切れて見えるようになるまで、同じグループの続きは返ってこない
  • 特定のグループIDを指定して受信することはできない

3つ目が見落とされがちです。「このテナントだけ先に処理する」という使い方は、FIFOキューの単一キューではできません。優先したいなら、キューを分ける設計になります。

偏ったグループが他を巻き込む

グループIDを粗く切りすぎると、1つのグループに処理待ちが積み上がります。SQSのドキュメントには、FIFOキューは先頭の120,000件を調べて、取り出せるグループを探すとあります。1つのグループに大きなバックログが溜まると、後から送られた他のグループのメッセージが、そのバックログの処理が終わるまで取り出せなくなることがあります。

原因として挙げられているのは、コンシューマーが同じメッセージの処理に繰り返し失敗するケースです。内容の問題でも、コンシューマー側の障害でも起こります。対策は、デッドレターキューを設定して、失敗が続いたメッセージを退避させることです。同じグループの後続が詰まったままになるのを防げます。

また、処理中(受信済みで未削除)のメッセージは、キュー全体で最大120,000件です。上限に達してもエラーは返らず、処理が影響を受けるだけです。「どれだけ並列に取るか」を決めるときに、この数字も計算に入れます。

重複排除IDは「5分間の約束」

MessageDeduplicationIdは、5分間の重複排除窓の中で、同じIDのメッセージを1件しか配信しないための値です。同じIDで2回目が来ても、送信は成功扱いで受け付けられますが、コンシューマーには届きません。

注意点が2つあります。

  • メッセージを受信して削除した後も、SQSはそのIDを追跡し続ける
  • 窓は5分で、その外で同じ内容を送れば別のメッセージとして届く

後者は、重複排除が「送信側のリトライ」を守る仕組みであって、受信側の冪等性の代わりではないことを意味します。5分を超えた再送や、コンシューマーが処理中に失敗して再配信される場面は、別の層で守る必要があります。受信側の考え方はStripe webhookの冪等受信の記事が参考になります。

コンテンツベース重複排除の落とし穴

コンテンツベース重複排除を有効にすると、SQSはメッセージ本文のSHA-256ハッシュから重複排除IDを作ります。ハッシュの対象は本文だけで、メッセージ属性は含まれません。

ここから、設計上の落とし穴が3つ出ます。

落とし穴

重複排除IDを自前で付けたほうがよい場面

  • 同じ本文を別メッセージとして送りたい

    本文が同一のメッセージは、5分以内なら2通目が落ちます。たとえば「再計算を要求する」のような中身のない命令を連続で送る場合です。

  • 本文が同じで属性だけが違う

    属性は重複排除の判断に入りません。属性に宛先やバージョンを持たせているなら、重複排除IDを明示します。

  • 本文が違っても重複として扱いたい

    本文にリトライ回数を入れると、リトライのたびに本文が変わり、重複排除が効きません。業務上のイベントIDを重複排除IDに使います。

どれも、SQSが重複排除IDの付け方として挙げている場面そのものです。明示した重複排除IDは、コンテンツベースが有効なキューでも、生成されるIDより優先されます。

結論として、本文にイベントIDや業務キーが入っていて、1メッセージ1イベントが保証できるなら、コンテンツベースで足ります。それ以外は、業務上のイベントIDを明示する設計が安全です。

テストで挙動を確かめる

挙動の理解は、手元のキューで確かめられます。ここでは、使い捨てのFIFOキューを作って4つの性質を検証するpytestを例に示します。AWSのドキュメントに書かれた性質を、そのまま検査する形です。

pip install boto3 pytest
pytest test_sqs_fifo.py -q
# test_sqs_fifo.py(例。開発用アカウントの使い捨てキューで実行する)
import time
import boto3
import pytest
 
sqs = boto3.client("sqs")
 
 
@pytest.fixture
def queue():
    name = f"design-test-{int(time.time())}.fifo"
    url = sqs.create_queue(
        QueueName=name,
        Attributes={
            "FifoQueue": "true",
            "ContentBasedDeduplication": "true",
            "VisibilityTimeout": "30",
        },
    )["QueueUrl"]
    yield url
    sqs.delete_queue(QueueUrl=url)
 
 
def send(q, body, group, dedup=None, attrs=None):
    kw = dict(QueueUrl=q, MessageBody=body, MessageGroupId=group)
    if dedup:
        kw["MessageDeduplicationId"] = dedup
    if attrs:
        kw["MessageAttributes"] = attrs
    return sqs.send_message(**kw)
 
 
def receive(q, n=10):
    r = sqs.receive_message(
        QueueUrl=q, MaxNumberOfMessages=n, WaitTimeSeconds=2
    )
    return r.get("Messages", [])
 
 
def test_同じ本文は5分窓で1件に畳まれる(queue):
    send(queue, "recalc", "g1")
    send(queue, "recalc", "g1")
    assert len(receive(queue)) == 1
 
 
def test_重複排除IDを明示すれば同じ本文でも2件(queue):
    send(queue, "recalc", "g1", dedup="evt-1")
    send(queue, "recalc", "g1", dedup="evt-2")
    assert len(receive(queue)) == 2
 
 
def test_属性だけが違っても重複扱い(queue):
    a = {"v": {"DataType": "String", "StringValue": "1"}}
    b = {"v": {"DataType": "String", "StringValue": "2"}}
    send(queue, "same-body", "g1", attrs=a)
    send(queue, "same-body", "g1", attrs=b)
    assert len(receive(queue)) == 1
 
 
def test_同じグループは処理中に次が返らない(queue):
    send(queue, "m1", "g1")
    send(queue, "m2", "g1")
    first = receive(queue, n=1)
    assert len(first) == 1
    assert receive(queue, n=1) == []  # 削除か可視性切れまで待たされる
    sqs.delete_message(
        QueueUrl=queue, ReceiptHandle=first[0]["ReceiptHandle"]
    )
    assert len(receive(queue, n=1)) == 1
 
 
def test_別グループなら並行して取れる(queue):
    send(queue, "m1", "g1")
    send(queue, "m2", "g2")
    assert len(receive(queue, n=1)) == 1
    assert len(receive(queue, n=1)) == 1

これは公式の記述に沿った検査の例です。実際の件数は、実行したキューでの結果が正です。まず1回流して、期待と違うテストがあれば、設計の前提を見直す材料にします。

5分窓そのものを時間で確かめるには

5分を超えた再送が別メッセージになる性質は、テストに5分の待機を入れる必要があります。毎回の実行に入れると遅いので、普段は外し、pytest -m slowのようにマークで分けておくのが現実的です。デプロイ前に1回だけ流す位置づけにします。

Claude Codeに渡す設定

テストがあれば、Claude Codeは自分で結果を読み、直して、もう一度流せます。Claude Codeのベストプラクティスにも、合否が返ってくるチェックを渡すと、Claudeが自分でループを閉じられると書かれています。

CLAUDE.mdには前提と実行コマンドだけを書く

CLAUDE.mdは短く保つのが基本です。コードから読み取れないことと、確認のコマンドに絞ります。

# Commands
- SQS設計テスト: pytest test_sqs_fifo.py -q
 
# SQS FIFOの前提
- MessageGroupIdは注文IDで切る(顧客単位にしない)
- 重複排除IDは業務のイベントIDを明示する。本文ハッシュに頼らない
- 本番キューには触らない。テストは使い捨てのFIFOキューだけ

本番への操作は権限ルールで止める

「本番キューに触らない」は、CLAUDE.mdに書くだけだと、Claudeが適用外と判断する余地が残ります。例外のないルールは、権限ルールに置きます。たとえば次のような設定です。

{
  "permissions": {
    "deny": [
      "Bash(aws sqs purge-queue *)",
      "Bash(aws sqs delete-queue *)"
    ],
    "ask": ["Bash(aws sqs send-message *)"]
  }
}

denyは権限モードによらず効きます。ルールが照合するのはツール呼び出しで、意図ではありません。AWS SDKを呼ぶスクリプトの中身までは止められません。環境変数でプロファイルを開発用に固定するなど、実行側の分離も合わせます。

Stopフックで「テストが通るまで終わらせない」

テストの合格を強制したいなら、Stopフックを使います。終了コード2を返すと、Claudeは止まらず作業に戻ります。標準エラーに出した内容が、止められた理由として渡されます。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "pytest test_sqs_fifo.py -q 1>&2 || exit 2"
          }
        ]
      }
    ]
  }
}

このフックが連続8回止めると、Claude Codeが次の停止を通します。上限はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられます。AWSの認証情報がない環境では、フックが毎回失敗するので、開発用プロファイルが使える状態であることを先に確かめます。ファイルを書き換えるたびにテストを回したい場合は、PostToolUseとStopの使い分けの記事が手がかりになります。

Claudeへの頼み方

設計を任せるときは、前提を先に渡します。

SQS FIFOのキューを設計して。
- 順序が必要なのは同じ注文の中だけ。注文をまたぐ順序は不要
- 同時に処理したい注文は数百件
- 重複送信は5分以内に起きうるが、同じ本文を意図的に続けて送る命令もある
test_sqs_fifo.pyを先に書いて、実行して落ちる理由を読んでから実装して。

「順序が必要な範囲」「同時に動かしたい数」「同じ本文を意図的に送る場面」の3つが、グループIDと重複排除IDの設計をほぼ決めます。この3つが曖昧なままだと、グループIDを何にするかをClaudeが推測で決めることになります。

生成された設計は、そのまま信じず、テストの出力を見て判断します。コマンドと出力を一緒に出すよう頼んでおくと、「通りました」だけの報告より検証が速くなります。

設計の切り分け早見表

状況グループID重複排除ID
注文ごとに順序が必要グループID注文ID重複排除IDイベントID
全体で1本の順序が必要グループID固定値(並列度は1)重複排除IDイベントID
順序は不要で重複排除だけ欲しいグループID順序不要ならstandardキューも検討重複排除IDイベントID
本文にユニークな業務キーがあるグループID業務キー単位重複排除IDコンテンツベースで可
特定テナントだけ優先したいグループIDキューを分ける重複排除IDイベントID

2行目の固定値は、ドキュメントが「すべてのメッセージを厳密な順序で処理したい場合は、全メッセージで同じグループIDを使う」としている構成です。並列度が1になるので、処理時間が長いと待ち行列が伸びます。

まとめ

グループIDは順序の範囲と並列度を同時に決め、偏りはバックログの詰まりにつながります。重複排除IDは5分窓の約束であり、本文ハッシュに任せると、同じ本文と属性違いの扱いで足をすくわれます。これらの性質は、使い捨てのキューに対するテストで確かめられます。

Claude Codeには、前提・テスト・実行コマンドを渡し、本番操作はdenyで止め、合格までをStopフックで保つ構成が合います。5分窓を超えた重複は残るため、コンシューマー側の冪等性は別途用意します。DynamoDBと組み合わせる場合のキー設計は、DynamoDBのテーブル設計の記事にアクセスパターンからの詰め方があります。

参照した一次ソース

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

  1. code.claude.com/docs/en/best-practices
  2. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messa…
  3. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messa…
  4. docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues…
この記事を共有:XはてブLinkedIn