Lambda-Backed Custom Resourceを利用してServerless Application Modelで定義したLambda関数にDead Letter Queueを設定する

2017年3月29日追記
米国時間3月28日のアップデートでAWS::Lambda::FunctionリソースがDeadLetterConfigプロパティをサポートしました。

はじめに

こんにちは、中山です。

引き続きServerless Application Model(以下AWS SAM)ネタです。今回はAWS SAMで定義したLambda関数にDead Letter Queueを設定する方法をご紹介します。Dead Letter Queueそのものについては以下のエントリを参照してください。

残念ながら執筆時点(2017/02/14)では、AWS SAMでLambda関数を定義するためのAWS::Serverless::Function及びAWS::Lambda::FunctionがDead Letter Queue用のプロパティをサポートしていません。その内サポートされると思いますが、すぐに使いたい場合は自分で作り込む必要があります。AWS SAMの対抗フレームワークであるServerless Frameworkでは、プラグインという形で設定可能です。以下のエントリに内容をまとめています。

では、AWS SAMの場合どうやるのでしょうか。やはりそこはみんな大好きLambda-Backed Custom Resourceですね。AWS SAMで定義したLambda関数を、Lambda-Backed Custom ResourceでアップデートしてDead Letter Queueを設定可能です。今回簡単なサンプルを交えて具体的にご紹介したいと思います。

なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。

  • AWS SAM: 2016-10-31
  • AWS CLI: aws-cli/1.11.47 Python/2.7.12 Darwin/16.4.0 botocore/1.5.10

やってみる

早速やってみましょう。

ディレクトリ構成

今回は以下のようなディレクトリ構成にします。

$ tree .
.
├── sam.yml
└── src
    ├── handlers
    │   ├── failure
    │   │   └── index.py
    │   └── invoked
    │       └── index.py
    └── templates
        ├── custom.yml
        ├── sns.yml
        └── sqs.yml

5 directories, 6 files

トップディレクトリにAWS SAM用テンプレート、 src/handlers 以下にLambda関数、 src/templates 以下にCloudFormationテンプレートを設置しています。このディレクトリ配置にした主な理由は以下のエントリにまとめているので、よろしければ参照してください。

トップディレクトリの sam.yml はもちろん単なるCloudFormationテンプレートなので、 src/templates 以下のテンプレートと分けなくても問題ありません。しかし、CloudFormationのベストプラクティス的には各種コンポーネント毎にスタックを分けることが推奨されている点、また詳細は後述しますが aws cloudformation package コマンドでスタックのネストがとても使いやすくなった、という点からこの構成にしています。

テンプレートの内容

  • sam.yml
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: AWS SAM

Resources:
  SQS:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/sqs.yml
  SNS:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/sns.yml

  Failure:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/failure
      Handler: index.handler
      Runtime: python2.7
      Policies:
        - Version: 2012-10-17
          Statement:
            - Sid: SQSSendMessagePolicy
              Effect: Allow
              Action: sqs:SendMessage
              Resource: !GetAtt SQS.Outputs.QueueArn
            - Sid: SNSPublishPolicy
              Effect: Allow
              Action: sns:Publish
              Resource: !GetAtt SNS.Outputs.TopicArn
      Events:
        Timer:
          Type: Schedule
          Properties:
            Schedule: rate(1 minute)
  DeadLetterQueue:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/custom.yml
      Parameters:
        FunctionName: !Ref Failure
        QueueArn: !GetAtt SQS.Outputs.QueueArn
        TopicArn: !GetAtt SNS.Outputs.TopicArn

  Invoked:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/invoked
      Handler: index.handler
      Runtime: python2.7
      Events:
        Invoker:
          Type: SNS
          Properties:
            Topic: !GetAtt SNS.Outputs.TopicArn

AWS::CloudFormation::Stackリソースでこのテンプレートから別のスタックを作成しています。 TemplateURL プロパティに注目してください。このプロパティの値はS3に保存されたテンプレートへのパスを指定しなければならないのですが、ローカルファイルへのパスを設定しています。 aws cloudformation package を実行すると、パスで指定されたテンプレートをS3にアップロードし、プロパティの値をS3へのパスへ自動で変換することが可能です。例えば以下のような形になります。

  SNS:
    Properties:
      TemplateURL: https://s3.amazonaws.com/<_YOUR_S3_BUCKET_>/e9711c8c48b6ea7ddb2fffc39f9fe2ed.template
    Type: AWS::CloudFormation::Stack

このコマンドが登場するまではスタックのネストが非常に使いづらい状況でした。テンプレートの修正後S3にアップロードするといった作業を手動で繰り返さなければならないからです。ローカルファイルへのパスを指定できるようになったことでかなり使いやすくなったなと感じています。

Failure という論理リソースIDでDead Letter Queueを設定するLambda関数を定義しています。SQSキューに対して sqs:SendMessage を、SNSトピックに対して sns:Publish 権限を付与しています。また、動作確認をしやすくするために Events プロパティでCloudWatch Eventを指定しました。

DeadLetterQueue という論理リソースIDでLambda-Backed Custom Resourceを定義したテンプレートからスタックを作成しています。内容は後述しますが、Dead Letter Queueを設定するLambda関数名と、その宛先となるSQSキューARN/SNSトピックARNをパラメータで渡しています。

Invoked という論理リソースIDでSNSトピックから呼び出されるLambda関数を定義しています。EventsプロパティでSNSタイプを設定しているため、Dead Letter Queueの宛先にSNSトピックを指定した場合、このLambda関数が起動されることになります。

  • src/templates/sns.yml
---
AWSTemplateFormatVersion: 2010-09-09
Description: SNS

Resources:
  Topic:
    Type: AWS::SNS::Topic

Outputs:
  TopicArn:
    Value: !Ref Topic

Dead Letter Queueの宛先となるSNSトピックを作成し、そのARNをアウトプットさせているだけです。

  • src/templates/sqs.yml
---
AWSTemplateFormatVersion: 2010-09-09
Description: SQS

Resources:
  Queue:
    Type: AWS::SQS::Queue

Outputs:
  QueueArn:
    Value: !GetAtt Queue.Arn

こちらも同様にキューの作成とARNのアウトプットをしています。

  • src/templates/custom.yml
---
AWSTemplateFormatVersion: 2010-09-09
Description: Dead Letter Queue

Parameters:
  FunctionName:
    Type: String
  QueueArn:
    Type: String
  TopicArn:
    Type: String

Resources:
  LambdaBasicExecRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: LambdaAssumeRolePolicy
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSLambdaFullAccess
  LambdaDeadLetterQueue:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import cfnresponse
          import boto3
          import json
          def handler(event, context):
              if event['RequestType'] == 'Delete':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              function_name = event['ResourceProperties']['FunctionName']
              target_arn = event['ResourceProperties']['TargetArn']
              try:
                  resp = boto3.client('lambda').update_function_configuration(
                          FunctionName=function_name,
                          DeadLetterConfig={'TargetArn': target_arn})
              except:
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
              else:
                  response_data = {'Response': json.dumps(resp)}
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Handler: index.handler
      Role: !GetAtt LambdaBasicExecRole.Arn
      Runtime: python2.7
  CustomDeadLetterQueue:
    Type: Custom::DeadLetterQueue
    Version: 1.0
    Properties:
      ServiceToken: !GetAtt LambdaDeadLetterQueue.Arn
      FunctionName: !Ref FunctionName
      TargetArn: !Ref QueueArn
      #TargetArn: !Ref TopicArn

このテンプレートが本エントリの本題です(ここまでの説明が長くなりましたが)。 LambdaDeadLetterQueue でLambda関数を、 CustomDeadLetterQueue でそれを呼び出すカスタムリソースを定義しています。インラインで記述したLambda関数の内容は単純です。Boto3のupdate_function_configurationメソッドを利用して関数の設定をアップデートさせているだけです。こうすることで AWS::Serverless::Function で定義したLambda関数の設定をアップデートできるという訳です。

Lambda関数自体はシンプルに以下のようにしました。1つ目は常に関数の実行を失敗させています。2つ目は event の内容を出力させているだけです。

  • src/handlers/failure/index.py
def handler(event, context):
    raise Exception('Exception occured')
  • src/handlers/invoked/index.py
from __future__ import print_function


def handler(event, context):
    print('Invoked with this event: {}'.format(event))

動作確認

Dead Letter Queueは現状1つしか指定できないので、SQSから確認してみます。まずはテンプレートの変換とアーティファクトのアップロードを実施します。トップディレクトリ上で以下のコマンドを実行してください。変換されたテンプレートはデプロイ以外では必要ないので、 .sam などのディレクトリに置いてVCSの管理外にしておくとよいと思います。

$ aws cloudformation package \
  --template-file sam.yml \
  --s3-bucket <_YOUR_S3_BUCKET_> \
  --output-template-file .sam/packaged.yml

スタックを作成します。

$ aws cloudformation deploy \
  --template-file .sam/packaged.yml \
  --stack-name dead-letter-queue \
  --capabilities CAPABILITY_IAM

計4つのスタックが作成されます。

$ aws cloudformation list-stacks \
  --stack-status-filter CREATE_COMPLETE \
  --query 'StackSummaries[?contains(StackName,`dead-letter-queue`)].StackName'
[
    "dead-letter-queue-DeadLetterQueue-1G2G5WY2PFN3E",
    "dead-letter-queue-SNS-1Q8QPI3W6NOZQ",
    "dead-letter-queue-SQS-MJSS8933C46S",
    "dead-letter-queue"
]

Lambda関数の設定を確認してみます。 TargetArn にSQSキューのARNが指定されていることを確認できます。

$ aws lambda get-function-configuration \
  --function-name "$(aws lambda list-functions \
    --query 'Functions[?contains(FunctionName,`dead-letter-queue-Failure`)].FunctionName' \
    --output text)"
{
    "CodeSha256": "UsH0yYCDIX+/bqK44BEorhPz0AKZJuKj8lO1ZYZBKQA=",
    "FunctionName": "dead-letter-queue-Failure-1CUIKP61Z8K66",
    "CodeSize": 177,
    "MemorySize": 128,
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:************:function:dead-letter-queue-Failure-1CUIKP61Z8K66",
    "Version": "$LATEST",
    "Role": "arn:aws:iam::************:role/dead-letter-queue-FailureRole-14A11UV6SD531",
    "Timeout": 3,
    "LastModified": "2017-02-14T11:17:19.781+0000",
    "Handler": "index.handler",
    "DeadLetterConfig": {
        "TargetArn": "arn:aws:sqs:ap-northeast-1:************:dead-letter-queue-SQS-MJSS8933C46S-Queue-1H4YEWO8ECVJP"
    },
    "Runtime": "python2.7",
    "Description": ""
}

少し待つとキューにメッセージが溜まっていくことが確認できます。

$ aws sqs get-queue-attributes \
  --queue-url "$(aws sqs list-queues \
    --query 'QueueUrls' \
    --output text)" \
  --attribute-names ApproximateNumberOfMessages
{
    "Attributes": {
        "ApproximateNumberOfMessages": "5"
    }
}

続いてDead Letter Queueの宛先にSNSを設定してみましょう。 src/templates/custom.ymlTargetArn: !Ref TopicArn のように修正して再度テンプレートの変換とアップロード/デプロイを実行してください。スタックの更新が完了すると、以下のように TargetArn にSNSトピックのARNが設定されると思います。

$ aws lambda get-function-configuration \
  --function-name "$(aws lambda list-functions \
    --query 'Functions[?contains(FunctionName,`dead-letter-queue-Failure`)].FunctionName' \
    --output text)"
{
    "CodeSha256": "UsH0yYCDIX+/bqK44BEorhPz0AKZJuKj8lO1ZYZBKQA=",
    "FunctionName": "dead-letter-queue-Failure-1CUIKP61Z8K66",
    "CodeSize": 177,
    "MemorySize": 128,
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:************:function:dead-letter-queue-Failure-1CUIKP61Z8K66",
    "Version": "$LATEST",
    "Role": "arn:aws:iam::************:role/dead-letter-queue-FailureRole-14A11UV6SD531",
    "Timeout": 3,
    "LastModified": "2017-02-14T11:04:55.606+0000",
    "Handler": "index.handler",
    "DeadLetterConfig": {
        "TargetArn": "arn:aws:sns:ap-northeast-1:************:dead-letter-queue-SNS-1Q8QPI3W6NOZQ-Topic-RXM2F1OYAE4D"
    },
    "Runtime": "python2.7",
    "Description": ""
}

しばらく待った後に、SNSトピックから起動されるLambda関数のCloudWatch Logsを確認すると、正常に起動されていることが確認できます。

$ aws logs get-log-events \
  --log-group-name /aws/lambda/dead-letter-queue-Invoked-1M4Q6QTZC9POI \
  --log-stream-name '2017/02/14/[$LATEST]********************************'
<snip>
        {
            "ingestionTime": 1487070326578,
            "timestamp": 1487070311392,
            "message": "Invoked with this event: {u'Records': [{u'EventVersion': u'1.0', u'EventSubscriptionArn': u'arn:aws:sns:ap-northeast-1:************:dead-letter-queue-SNS-1Q8QPI3W6NOZQ-Topic-RXM2F1OYAE4D:f52640d5-8d84-4051-9419-91b59b8f7d8b', u'EventSource': u'aws:sns', u'Sns': {u'SignatureVersion': u'1', u'Timestamp': u'2017-02-14T11:05:10.953Z', u'Signature': u'FentwiyVJt6N/z6sAa5mXU+HMbmcvhGvJKbvcPrqAIhjyahswUHjQ4MrG6pwc4T0kYzRrithfXsedJlJ2kgDRMeDWLYhGg2XczijPV4WzguIW7IXIrV5z5w3p/D+SB7kjdOxlAuBHsZ6Q7WJbSpVVAjLYtaT17RnGMXGzEC2hBwzNbGX9Kvlac5vfe5ULMBgljIRik+H7k/qQFyKZ3edoiuq4IRhH+QSOXb4AUre3PJlzGRoWNkOdiiwyMQ54J9/++rZKR1JWC2JSMKXNxryl28/Juwebd9tWyUHnl2hOyPdyx2ckVUByvK46NmKbLw5NZYzQ+H5w05kpo+lDsPS0w==', u'SigningCertUrl': u'https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem', u'MessageId': u'ea445674-eb6c-5b01-9d08-07680dca07b4', u'Message': u'{\"version\":\"0\",\"id\":\"fa248b23-6277-4ae0-9701-3121a6a8ca03\",\"detail-type\":\"Scheduled Event\",\"source\":\"aws.events\",\"account\":\"************\",\"time\":\"2017-02-14T11:01:19Z\",\"region\":\"ap-northeast-1\",\"resources\":[\"arn:aws:events:ap-northeast-1:************:rule/dead-letter-queue-FailureTimer-G2B3KIPHXTIE\"],\"detail\":{}}', u'MessageAttributes': {u'ErrorCode': {u'Type': u'String', u'Value': u'200'}, u'ErrorMessage': {u'Type': u'String', u'Value': u'Exception occured'}, u'RequestID': {u'Type': u'String', u'Value': u'078a04f6-f2a5-11e6-bf49-9fb4922eecf8'}}, u'Type': u'Notification', u'UnsubscribeUrl': u'https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:************:dead-letter-queue-SNS-1Q8QPI3W6NOZQ-Topic-RXM2F1OYAE4D:f52640d5-8d84-4051-9419-91b59b8f7d8b', u'TopicArn': u'arn:aws:sns:ap-northeast-1:************:dead-letter-queue-SNS-1Q8QPI3W6NOZQ-Topic-RXM2F1OYAE4D', u'Subject': None}}]}\n"
        },
<snip>

まとめ

いかがだったでしょうか。

AWS SAMとLambda-Backed Custom Resourceを利用したDead Letter Queueの設定方法をご紹介しました。やはりLambda-Backed Custom Resourceは最高ですね。Dead Letter Queueサポート早く来てくれ!!!!1111

本エントリがみなさんの参考になれば幸いに思います。