LambdaのDLQ(デッドレターキュー)とDestinations(非同期呼び出しの宛先指定)を比較してみた

2020.06.29

非同期呼び出しのLambdaは、処理失敗時に他のAWSサービスを実行する方法として、DLQ(デッドレターキュー)と(失敗時の)Destinations(非同期呼び出しの宛先指定)があります。DLQは2016年12月にリリースされた機能、かたやDestinationsは2019年11月にリリースされた比較的新しい機能です。

この2つの機能の違い、またはどう使い分けるべきか調べてみました。実際に両機能を使ってみて、得られる情報の違いを比較します。

実装&テスト

Serverless Frameworkで実装します。前回のエントリで失敗時のDestinationsについては実装済なので、そこにDLQも追加し、関数実行してみます。

ハイライト部分がDLQとして追加した箇所です。

serverless.yml

service:
    name: destinations-sample

custom:
    webpack:
        webpackConfig: ./webpack.config.js
        includeModules: true
    fail-queue-name: fail-destination-queue-by-sls

plugins:
    - serverless-webpack
    - serverless-pseudo-parameters

provider:
    name: aws
    runtime: nodejs12.x
    region: ap-northeast-1
    environment:
        AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
    iamRoleStatements:
        - Effect: Allow
          Action: sqs:SendMessage
          Resource: !GetAtt DLQ.Arn

functions:
    hello:
        handler: handler.hello
        destinations:
            onFailure: arn:aws:sqs:#{AWS::Region}:#{AWS::AccountId}:${self:custom.fail-queue-name}
        onError: !GetAtt DLQ.Arn

resources:
    Resources:
        FailQueue:
            Type: "AWS::SQS::Queue"
            Properties:
                QueueName: ${self:custom.fail-queue-name}
                MessageRetentionPeriod: 1209600
        DLQ:
            Type: "AWS::SQS::Queue"
            Properties:
                QueueName: dlq-by-sls
                MessageRetentionPeriod: 1209600

Lambda関数は前回と同じです。イベントペイロードに{ "success": false }を渡すとエラーを発生させるコードです。

handler.ts

import "source-map-support/register";

export const hello = async (event) => {
    console.log(event);
    if (event.success === false) {
        throw new Error("destination test");
    }
    return true;
};

関数実行します。{ "success": false }を渡したので、再試行含め計3回実行した後、DLQに指定したdlq-by-slsキューとDestinationsに指定したfail-destination-queue-by-slsキューにそれぞれメッセージが送信されているはずです。

npx sls invoke -f hello -d '{ "success": false }' -t Event

メッセージ確認

DLQ

$ aws sqs receive-message \
> --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/dlq-by-sls \
> --attribute-names All \
> --message-attribute-names All \
> --max-number-of-messages 10
{
    "Messages": [
        {
            "MessageId": "14fe15eb-c097-4c59-82ae-b3812cfce169",
            "ReceiptHandle": "AQEBguEFsZi3HdPLKx1Wb0Ya946j6z3EhQsaDlwugvV6qZTn3MA6Xe+dFsRoiu/77C6iGhMDjfKDnTwKIPWXN5++lQm1XNv9/4qK9Tg/A7G62KgJF0HebToO0aP/kh1S8HF2FSv9r8c24OJsWkHlZnodf3YFQAY7lcq13+4F1y7iTgZtM08DhkWoHlNzsskfKChmjVgu8EkXO85TfOJd/1SjWPub/TSWlr819M2eGP5SWM4Do2abTNdC1sAUUKVnt3K58HqJFz5vv21a7OKs88SIbRBJoe10M8YckKumoKiTEN9v19UnLPW1ALr33RMvwwh4RuvHz8VVlxmdeW/tWXLIKWhJThMV5819BqhQAa0oJGoMe7QlyLQKAaKayKYIgT81ACE/yrKQrhls73HbD6Z5Dg==",
            "MD5OfBody": "c0a4229f65148628b26e451304ddac68",
            "Body": "{\"success\":false}",
            "Attributes": {
                "SenderId": "AROAQWCYJEBRJM6ORNHK5:awslambda_800_20200628065429546",
                "ApproximateFirstReceiveTimestamp": "1593327635445",
                "ApproximateReceiveCount": "1",
                "SentTimestamp": "1593327269630"
            },
            "MD5OfMessageAttributes": "7df843cefe0509f157ec71d0877ad60d",
            "MessageAttributes": {
                "ErrorCode": {
                    "StringValue": "200",
                    "DataType": "Number"
                },
                "ErrorMessage": {
                    "StringValue": "destination test",
                    "DataType": "String"
                },
                "RequestID": {
                    "StringValue": "b1084703-4304-4a53-ad7e-c1fa189ef921",
                    "DataType": "String"
                }
            }
        }
    ]
}

Destinations

$ aws sqs receive-message \
> --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/fail-destination-queue-by-sls  \
> --attribute-names All \
> --message-attribute-names All \
> --max-number-of-messages 10 \
{
    "Messages": [
        {
            "MessageId": "24f74995-8de9-43bc-a832-46814d58e6d6",
            "ReceiptHandle": "AQEBFxDcx7eHBBzT+60qDt7jdAAyMTtM56agiXbi1i5ABvXJw3qG8/2We5Io5ev8586Lc0T1hkZ/PA1s1loz6+rNCGXjZ5EVD4YlaYnNSxKqskNI3p6XM86lXPn8RyeXL5YVDNo6NCiMaUq0BmifxUjnz1Py6oyR3cbd/USYuKwztBS6tBO7GJd/qZjSA4bhE8bP/zkPK7xhxxcG8mZ5Jodiq3xamGlCTlLLUGSqLarakJ0P6S/1L06aii5N7nZj4unmAilangnJF/9/tNdNcUDxIO9Jg5Jd6eW0EZn63h1fIlTdFOykDrJb3Q0W812NY0M2tMDfr+nvBKxHYc5Fm/10QzO53EYPFD4x0UlrOoN2WS9O7j6tBT3EfVB+uJruneOkMGwKlbFvoXEWLy3vgfqXq3tYDIoRC/kfmhbWPItnbDg=",
            "MD5OfBody": "70243e28b6f79f34491efd1abeba7752",
            "Body": "{\"version\":\"1.0\",\"timestamp\":\"2020-06-28T06:54:29.645Z\",\"requestContext\":{\"requestId\":\"b1084703-4304-4a53-ad7e-c1fa189ef921\",\"functionArn\":\"arn:aws:lambda:ap-northeast-1:123456789012:function:destinations-sample-dev-hello:$LATEST\",\"condition\":\"RetriesExhausted\",\"approximateInvokeCount\":3},\"requestPayload\":{\"success\":false},\"responseContext\":{\"statusCode\":200,\"executedVersion\":\"$LATEST\",\"functionError\":\"Unhandled\"},\"responsePayload\":{\"errorType\":\"Error\",\"errorMessage\":\"destination test\",\"trace\":[\"Error: destination test\",\"    at Runtime.n [as handler] (/var/task/webpack:/handler.ts:6:15)\",\"    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)\"]}}",
            "Attributes": {
                "SenderId": "AROAQWCYJEBRJM6ORNHK5:awslambda_800_20200628065429546",
                "ApproximateFirstReceiveTimestamp": "1593327594714",
                "ApproximateReceiveCount": "2",
                "SentTimestamp": "1593327269660"
            }
        }
    ]
}

Body部をパースしてみます。

$ aws sqs receive-message \
> --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/fail-destination-queue-by-sls  \
> --attribute-names All \
> --message-attribute-names All \
> --max-number-of-messages 10 \
> | jq -r .Messages[0].Body \
> | jq
{
  "version": "1.0",
  "timestamp": "2020-06-28T06:54:29.645Z",
  "requestContext": {
    "requestId": "b1084703-4304-4a53-ad7e-c1fa189ef921",
    "functionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:destinations-sample-dev-hello:$LATEST",
    "condition": "RetriesExhausted",
    "approximateInvokeCount": 3
  },
  "requestPayload": {
    "success": false
  },
  "responseContext": {
    "statusCode": 200,
    "executedVersion": "$LATEST",
    "functionError": "Unhandled"
  },
  "responsePayload": {
    "errorType": "Error",
    "errorMessage": "destination test",
    "trace": [
      "Error: destination test",
      "    at Runtime.n [as handler] (/var/task/webpack:/handler.ts:6:15)",
      "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
  }
}

比較

Destinationsの方が情報量が多い

DLQになくてDestinationsにある情報は以下です。

  • errorタイプ
  • Trace情報
  • requestContext
    • Lambda関数のARN
    • condition - なぜ失敗判定されたか、ということかと思われます。今回の場合 RetriesExhaustedなので、規定のリトライ回数再試行したけど処理成功しなかったから、ということです。
    • approximateInvokeCount - 処理試行回数
  • responseContext
    • ステータスコード
    • Lambda関数のバージョン
    • functionError

逆にDLQにある情報はすべてDestinationsに含まれています。

DLQはMessageAttributesにも情報がある

DLQはMessageAttributes(メッセージ属性)にも情報があります。というかBodyにはイベントペイロードしかないという潔さです。 Body内部ではなくMessageAttributesに情報があることで、若干目的の情報にたどり着くのが簡単になるでしょう。とくに以下のようにコンソールで確認する際は見やすいです。

リトライ処理はDLQの方が少しだけ簡単?

前述のとおりBodyにはイベントペイロードしか記載されていないので、それを使ってのLambda関数再実行はDLQの方が(やや)簡単に実装できるかと思います。以下例です。

$ npx sls invoke -f hello -d \
> "$(aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/dlq-by-sls  --max-number-of-messages 1\
> | jq -r .Messages[0].Body)" \
> -t Event

とはいえDestinationsもjqをもう一度かませばよいだけなので、これは本当に微々たる差ですね。。

$ npx sls invoke -f hello -d \
> "$(aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/dlq-by-sls  --max-number-of-messages 1\
> | jq -r .Messages[0].Body)\
> | jq -r .requestPayload)" \
> -t Event

と思ったのですが、Destinationsの方が簡単かもと思う部分も見つけました。
DLQのメッセージにはエラーが起きたLambda関数についての情報がありませんでした。ですので、どのLambda関数から送られてきたメッセージか、という部分はユーザー側で管理する必要がありそうです。大抵の場合SQSキュー名とLambda関数名を関連付けてわかりやすくされていると思いますが、そうしていない場合は辛そうです。

その他の違い

Destinationsの方が配信先のサービスが多い

DLQはSNSトピックと、SQSキューに対して配信可能です。一方のDestinationsはSNSトピックとSQSキューに加えLambda関数、EventBridgeにも配信が可能です。

バージョン

Lambda関数のバージョニングを使用する場合、DLQ、Destinations両方ともバージョンごとに設定が可能です。つまりバージョンが異なれば違う宛先に配信することができます。 しかし、DLQはバージョン毎に設定がロックされます。つまり過去バージョンのDLQの設定(宛先)を変更することはできません。一方Destinationsは変更可能です。

まとめ

DLQと(失敗時の)Destinationsについて比べてみました。情報量の多さ、連携できるAWSサービスの多さから、多くの場合でDestinationsを使うほうが良いかと思います。

参考情報