Amazon Web Services ブログ

Amazon SNS, Amazon SQS, AWS Lambda のデッドレターキューによる耐久性のあるサーバーレスアプリケーション設計

この投稿は Otavio Ferreira, Sr Manager, SNS の寄稿によるものです

郵便システムにおいて、デッドレターキューは配信不能な郵便物を取り扱うための施設です。pub/sub メッセージングモデルにおけるデッドレターキュー (DLQ: dead-letter-queue) は、トピックに対して発行されたメッセージがサブスクライブしているエンドポイントに配信できなかった場合に、そのメッセージを送ることができるキューを表します。

Amazon SNS による DLQ サポートによって、アプリケーションはメッセージ配信における各種故障モードに対する、さらなる耐久力と回復力を持つことが可能になりました。

メッセージの配信失敗と再試行を理解する

Amazon SNSがサブスクライブされたエンドポイントにアクセス出来ない場合、メッセージの配信は失敗します。このような状況は大きく2つの原因によって引き起こされます:

  • クライアントエラー。ここでクライアント (メッセージ送信者) は SNS となります。
  • サーバーエラー。ここではサーバーは、例えば Amazon SQSAWS Lambda のようにサブスクリプションのエンドポイント (メッセージ受信者) をホストするシステムとなります。

クライアントエラー

クライアントエラーは、 SNS の保持しているメタデータが最新ではない場合に発生します。クライアントエラーの発生するよくある原因としては、エンドポイントの所有者がエンドポイントを削除した場合が挙げられます。例えば SNS に紐付いたサブスクリプションを削除することなく、SNS トピックにサブスクライブした SQS キューを削除してしまったような場合です。やはりよくある別の例としては、エンドポイントに適用されたポリシーに対して、SNS がメッセージを配信することを阻害するような変更を加えてしまった場合が挙げられます。

これらのエラーは、クライアントがメッセージの配信を試みたにもかかわらず、クライアントの視点からエンドポイントがアクセス不能となっていることが原因で発生するため、クライアントエラーとして取り扱われます。SNS はクライアントエラーの結果として失敗したメッセージの配信を再試行することはありません。

サーバーエラー

サーバーエラーは、サブスクライブしているエンドポイントを実行しているサーバーが利用できないか、または SNS からの有効なリクエストを処理できなかったことを表す例外応答を返した場合に発生します。

サーバーエラーが発生した場合、SNSは線形、指数的のいずれかのバックオフ機能に基づいて配信を再試行します。SQS や Lambda 上で実行される AWS が管理するエンドポイントに対してサーバーエラーが発生した場合、SNS は 23 日間に渡って 100,015 回の再試行を行います。

サーバーエラーはお客様が管理するエンドポイント、具体的には HTTP や SMS、モバイルプッシュ、E メールにおいても発生する可能性があります。SNS はこれらの種類のエンドポイントに対しても配信を再試行します。HTTP エンドポイントに対してはお客様が再試行ポリシーを定義することができますが、SMS、E メール、およびモバイルプッシュの各エンドポイントに対しては SNS が内部的に、6 時間に渡って 50 回の再試行ポリシーを定義します。

配信の再試行

SNS は単一のクライアントエラーや、対応する再試行ポリシーで規定された再試行回数を超過したメッセージに対するサーバーエラーを受け取る可能性があります。このような場合、SNS はメッセージを破棄します。SNS のサブスクリプションに対して DLQ を設定することで、クライアントやサーバーといったエラーの種別に関わらず、破棄の対象となったメッセージを保持することができます。DLQ を利用することで配信することができなかったメッセージに対してより多くのコントロールを得ることが可能になります。

ぞれぞれのプロトコルにおいて SNS によってサポートされる配信再試行ポリシーの詳細は Amazon SNS Message Delivery Retry をご覧ください。

AWS のサービスで DLQ を利用する

SNS や SQS、Lambda は、それぞれ異なる故障モードに対応するために DLQ をサポートしています。これら全ての DLQ は通常の SQS キューによって実現されます。

SNS では、DLQ はサブスクライブしたエンドポイントへの配信に失敗したメッセージを保持します。詳細は Amazon SNS Dead-Letter Queues をご覧ください。

SQS では、DLQ はコンシューマーアプリケーションで処理することに失敗したメッセージを保持します。この故障モードはプロデューサーとコンシューマーが通信に利用するプロトコルの一部を誤って解釈した場合などに引き起こされます。そのような場合、コンシューマーはキューからメッセージを受信しますが、メッセージが期待する構造やコンテンツを持たないために処理に失敗します。コンシューマーはメッセージをキューから取り除くこともできません。Redrive ポリシーで指定された受信回数を消化した後に、SQS はメッセージを DLQ へ退避することができます。詳細は Amazon SQS デッドレターキュー をご覧ください。

Lambda では、DLQ は Lambda 関数の非同期実行に失敗したメッセージを保持します。関数実行の失敗にはいくつかの原因が考えられます。関数のコードが例外を発生させる、タイムアウトする、メモリが枯渇する、などです。コードを実行しているランタイムがエラーに見舞われ停止することもあります。関数が同時実行数の上限に到達し、スロットルされるかもしれません。エラーのタイプに関わらず、エラーが発生した場合には、コードは完全に実行されている場合もあれば、一部のみ実行されている、または全く実行されていない場合もあり得ます。Lambda は非同期実行を2回再試行します。再試行回数を消化すると、Lambda はメッセージを DLQ に退避することができます。詳細は AWS Lambda 関数のデッドレターキュー をご覧ください。

SQS キューや Lambda 関数が SNS トピックにサブスクライブするファンアウトアーキテクチャを採用している場合には、SNS のサブスクリプションだけではなく、宛先となるキューや関数に対しても DLQ を設定することをお勧めします。このアプローチによってアプリケーションはメッセージの配信エラーだけではなく、メッセージ処理のエラーや関数の実行エラーに対しても回復性を得ることができます。

実際のユースケースにおける DLQ の適用

全てを組み合わせるとこのようになります。以下の図ではレンタカーアプリケーションを支えるサーバーレスバックエンドのアーキテクチャを表しています。これは SNS、SQS、Lambda を利用した耐久性をもったサーバーレスアーキテクチャーの例です。

Dead Letter Queue - DLQ SNS use case with architecture diagram

お客様がレンタカーの注文を行うと、アプリケーションはその内容を Amazon API Gateway 上のAPIにリクエストとして送ります。この REST API は Rental-Orders という SNS トピックに接続され、Amazon VPC のサブネットにデプロイされます。トピックはさらに、注文を並列処理のためにサブスクライブしている 2 つのエンドポイントにファンアウトします:

  • Amazon EC2 上で稼働する社内のフルフィルメントシステムにデータを送る Rental-Fulfilment という名前の SQS キュー
  • 注文情報を加工して、同じく Amazon EC2 上で稼働する社外の請求システムに伝送する Rental-Billing という名前の Lambda 関数

このサーバーレスバックエンド API の耐久性を向上させるために、以下の DLQ が設定されています:

  • サブスクライブしている SQS キューや Lambda が到達不能になった場合に注文情報を保持するための 2 つの SNS DLQ である Rental-Fulfilment-Fanout-DLQRental-Billing-Fanout-DLQ
  • フルフィルメントシステムが注文を処理することに失敗した場合に注文情報を保持するための SQS DLQ である Rental-Fulfilment-DLQ
  • 関数が注文情報の加工や請求システムへの伝送に失敗した場合に注文情報を保持するための Lambda DLQ である Rental-Billing-DLQ

DLQ にメッセージが到達した際に、問題判別のためにメッセージを調査することができます。その後エラーの原因に対処した上で DLQ をポーリングすることでメッセージの処理を再試行することが可能です。

サブスクリプションやキュー、関数に対して DLQ を設定するためには AWS Management Console, SDK, CLI, API, または AWS CloudFormation を利用することが出来ます。また DLQ をポーリングするには SDK, CLI, そして API が利用できます。

サブスクリプションに対して DLQ を構成する

SNS サブスクリプションに DLQ を構成するには、サブスクリプションに対して RedrivePolicy パラメーターを設定します。このポリシーは DLQ の ARN を参照する JSON オブジェクトです。ARN は SNS サブスクリプションと同一アカウント内の SQS キューを指す必要があります。またサブスクリプションと DLQ は同一 AWS Region に属する必要があります。

ここでは先程ご紹介したレンタカーアプリケーションに対して SNS DLQ の一つを構成する方法をご紹介します。

次の JSON オブジェクトは SQS キュー Rental-Fulfilment を SNS トピック Rental-Orders にサブスクライブさせるための CloudFormation テンプレートです。このテンプレートはさらに Rental-Fulfilment-Fanout-DLQ を DLQ とするために RedrivePolicy もセットしています。

最後に、テンプレートは FilterPolicy をセットします。これによって発行されたメッセージが order-status 属性として confirmed または canceled を値として持つ場合にのみ SNS がサブスクライブしたキューへメッセージを配信します。Amazon SNS メッセージのフィルタ処理はメッセージ配信に先立って実施されるため、フィルターされたメッセージはサブスクリプションの DLQ には配信されません。

内部的には CloudFormation テンプレートは SNS の Subscribe API アクションを使用して、1 度のAPIリクエストでサブスクリプションと 2 つのポリシーを同時にデプロイします。

{  
   "Resources": {
      "mySubscription": {
         "Type" : "AWS::SNS::Subscription",
         "Properties" : {
            "Protocol": "sqs",
            "Endpoint": "arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment",
            "TopicArn": "arn:aws:sns:us-east-1:123456789012:Rental-Orders",
            "RedrivePolicy": {
               "deadLetterTargetArn": 
                  "arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ"
            },
            "FilterPolicy": { 
               "order-status": [ "confirmed", "canceled" ]
            }
         }
      }
   }
}

SNS トピックとサブスクリプションが既にデプロイされている場合もあります。その場合には、以下の AWS CLIAWS SDK for Java のコード例のように SNS の SetSubscriptionAttributes API アクションを使用して RedrivePolicy を設定することができます。

$ aws sns set-subscription-attributes 
   --region us-east-1
   --subscription-arn arn:aws:sns:us-east-1:123456789012:Rental-Orders:44019880-ffa0-4067-9cb4-b974443bcck2
   --attribute-name RedrivePolicy 
   --attribute-value '{"deadLetterTargetArn":"arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ"}'
AmazonSNS sns = AmazonSNSClientBuilder.defaultClient();

String subscriptionArn = "arn:aws:sns:us-east-1:123456789012:Rental-Orders:44019880-ffa0-4067-9cb4-b974443bcck2";

String redrivePolicy = "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ\"}";

SetSubscriptionAttributesRequest request = new SetSubscriptionAttributesRequest(
  subscriptionArn, 
  "RedrivePolicy", 
  redrivePolicy
);

sns.setSubscriptionAttributes(request);

DLQ のモニタリング

SNS サブスクリプションに関連付けられた DLQ を監視するには Amazon CloudWatch メトリクスアラーム を利用することができます。レンタカーの例では DLQ をモニタリングすることで、API がレンタカーの注文情報をフルフィルメントまたは請求システムのいずれかに配送することに失敗した場合に通知を受け取ることが可能になります。

通常の SQS キューと同様に、SNS の DLQ は NumberOfMessagesSentNumberOfMessagesReceivedNumberOfMessagesDeleted を始めとして様々なメトリクスを 5 分間隔のデータポイントとして CloudWatch に送ります。これらの SQS メトリクス を利用して、SNS の DLQ に動きがあった際に通知を受け取り、メッセージを回復するための手順を開始することができます。

DLQ は常に空であることが期待されるようなケースがあるかもしれません。その場合には NumberOfMessagesSent に対して閾値をゼロとして CloudWatch アラームを作成し、アラームが発報した際に通知される別の SNS トピックを用意します。この SNS トピックは、アラームの通知をさらに E メールアドレスや電話番号、モバイル通知アプリケーションなど任意のエンドポイントに配送することができます。

SNS 自身も DLQ に関連する独自のメトリクスを提供しています。具体的には SNS メトリクスは以下を含みます:

  • NumberOfNotificationsRedrivenToDlq – DLQ へのメッセージの送信が成功した場合に使用されます。
  • NumberOfNotificationsFailedToRedriveToDlq – DLQ へのメッセージの送信が失敗した場合に使用されます。これは DLQ が既に存在しないか、SNS がメッセージを送信するために必要な権限を与えられていない場合などに発生します。必要なアクセスポリシーを設定するための詳細な方法は Amazon SQS キューにメッセージを送信するアクセス許可を Amazon SNS トピックに付与する をご覧ください。

DLQ のデバッグ

CloudWatch Logs を使用することで SNS による配信が失敗しメッセージが DLQ へ退避される原因となった例外を確認することができます。レンタカーの例では、各 DLQ に格納された注文情報自体に加えてこれらのキューに関連付けられたログを調査することができます。これによってなぜ注文がフルフィルメントや請求システムへのファンアウトに失敗したのかを理解することが可能です。

SNS は配信の成功と失敗を CloudWatch に記録することができます。SNS トピックに配信プロトコル毎に固有の 3 つの属性をセットすることで Amazon SNS メッセージ配信ステータスのログ記録 を有効化することができます。例として SQS キューに対するSNS配信では以下のトピック属性を設定する必要があります: SQSSuccessFeedbackRoleArnSQSFailureFeedbackRoleArnSQSSuccessFeedbackSampleRate

以下の JSON オブジェクトは SNS 配信の成功を表す CloudWatch Logs のエントリーです。記録されるステータスコードは 200 (SUCCESS) です。RedrivePolicy 属性は対象の SNS サブスクリプションに対して DLQ が設定されていたことを表します。

{
  "notification": {
    "messageMD5Sum": "7bb3327ac55e49485bad42e159ca4d4b",
    "messageId": "e8c2bb09-235c-5f5d-b583-efd8df0f7d74",
    "topicArn": "arn:aws:sns:us-east-1:123456789012:Rental-Orders",
    "timestamp": "2019-10-04 05:13:55.876"
  },
  "delivery": {
    "deliveryId": "6adf232e-fb12-5062-a564-27ff3741051f",
    "redrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ\"}",
    "destination": "arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment",
    "providerResponse": "{\"sqsRequestId\":\"b2608a46-ccc4-51cc-003d-de972097debc\",\"sqsMessageId\":\"05fecd22-60a1-4d7d-bb79-026d49700b5a\"}",
    "dwellTimeMs": 58,
    "attempts": 1,
    "statusCode": 200
  },
  "status": "SUCCESS"
}

次の JSON オブジェクトは SNS 配信の失敗を表す CloudWatch Logs のエントリーです。この例コード例ではサブスクライブしたキューが存在しません。結果、クライアントエラーとしてステータスコード 400(FAILURE) が記録されます。繰り返しですが、RedrivePolicy 属性は DLQ の存在を示します。

{
  "notification": {
    "messageMD5Sum": "81c395cbd350da6bedfe3b24db9517b0",
    "messageId": "9959db9d-25c8-57a6-9439-8e5be8f71a1f",
    "topicArn": "arn:aws:sns:us-east-1:123456789012:Rental-Orders",
    "timestamp": "2019-10-04 05:16:51.116"
  },
  "delivery": {
    "deliveryId": "be743821-4c2c-5acc-a586-6cf0807f6fb1",
    "redrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ\"}",
    "destination": "arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment",
    "providerResponse": "{\"ErrorCode\":\"AWS.SimpleQueueService.NonExistentQueue\", \"ErrorMessage\":\"The specified queue does not exist or you do not have access to it.\",\"sqsRequestId\":\"Unrecoverable\"}",
    "dwellTimeMs": 53,
    "attempts": 1,
    "statusCode": 400
  },
  "status": "FAILURE"
}

メッセージの配信が失敗し、かつ DLQ がサブスクリプションにアタッチされている場合、メッセージは DLQ に送られ、CloudWatch に追加のエントリーが記録されます。この新たなエントリーは DLQ への配信に固有のもので、以下の JSON オブジェクトのように宛先として DLQ の ARN を参照します。

{
  "notification": {
    "messageMD5Sum": "81c395cbd350da6bedfe3b24db9517b0",
    "messageId": "8959db9d-25c8-57a6-9439-8e5be8f71a1f",
    "topicArn": "arn:aws:sns:us-east-1:123456789012:Rental-Orders",
    "timestamp": "2019-10-04 05:16:52.876"
  },
  "delivery": {
    "deliveryId": "a877c79f-a3ee-5105-9bbd-92596eae0232",
    "destination":"arn:aws:sqs:us-east-1:123456789012:Rental-Fulfilment-Fanout-DLQ",
    "providerResponse": "{\"sqsRequestId\":\"8cef1af5-e86a-519e-ad36-4f33252aa5ec\",\"sqsMessageId\":\"2b742c5c-0750-4ec5-a717-b95897adda8e\"}",
    "dwellTimeMs": 51,
    "attempts": 1,
    "statusCode": 200
  },
  "status": "SUCCESS"
}

Amazon CloudWatch Logs のエントリーを分析することで、SNS メッセージがなぜ DLQ に移されたのかを理解することが出来、メッセージを回復するために必要となるステップを実施することができます。SNS で配信ステータスのログ記録を有効化する際には、配信メッセージが記録されるサンプリングレートを 0% から 100% まで設定することができます。

DLQ の暗号化

SNS サブスクリプションが SQS の暗号化されたキューをターゲットとしている場合、DLQ も SQS の暗号化されたキューとして構成したほうが良いでしょう。これによって保管データに暗号化が適用されている状態に一貫性を持たせることができます。

このようなセキュリティ上の推奨に従うためには、 DLQ の暗号化に使用した CMK に対して、SNS のサービスプリンシパルによる AWS KMS APIアクションへのアクセス許可を付与するキーポリシー適用します。例として以下のキーポリシーを参照してください:

{
    "Sid": "GrantSnsAccessToKms",
    "Effect": "Allow",
    "Principal": { "Service": "sns.amazonaws.com" },
    "Action": [ "kms:Decrypt", "kms:GenerateDataKey*" ],
    "Resource": "*"
}

仮に SNS の暗号化されたトピックを利用していたとしても、サブスクリプションが暗号化された SQS キューではない DLQ を構成していた場合、DLQ に退避されたメッセージには保管データの暗号化が適用されません。

詳しくは 暗号化された Amazon SQS キューをサブスクライブして Amazon SNS トピックのサーバー側の暗号化 (SSE) を有効にする をご覧ください。

まとめ

SNS、SQS、Lambda に対して DLQ を設定することでアプリケーションの回復性と耐久性を高めることができます。これらの DLQ はそれぞれ異なる故障モードに対応しており、組み合わせて使用することができます。

  • SNS DLQ はサブスクライブしているエンドポイントへの配信に失敗したメッセージを保持します。
  • SQS DLQ はコンシューマーシステムが処理に失敗したメッセージを保持します。
  • Lambda DLQ は関数の非同期実行に失敗したメッセージを保持します。

サブスクリプションやキュー、関数に対する DLQ の設定は AWS Management Console、 SDK 、 CLI、 API、または CloudFormation から行うことができます。DLQ は全ての AWS リージョンでご利用いただけます。すぐに開始するには以下のチュートリアルを実施してください:

翻訳はソリューションアーキテクト石井が担当しました。原文はこちらです。