LambdaのリトライをAWS SQSを使ってやってみる

AWS事業本部の梶原@福岡です。

福岡オフィスでこんなことがありました。
「Lambdaファンクションで状態を取得して、正常に終了するまで、1分くらいでリトライしたいんですけどなんか簡単な方法ないすか?」
「CloudWatch Events で定期実行とか。」
「ステップファンクションでステートマシンで回すとか。」
「SQSとか?」
「SQS!!!!!!??????、S?Q?S?なぜ繰り返しでキューが!?」

実は昨年、SQSでアップデートがありました。 AWS LambdaがSQSをイベントソースとしてサポートしました!

AWS LambdaがSQSをイベントソースとしてサポートしました!

ということで、SQSからLambdaが起動できるんです。
え、だから、どうするのって事なんですが、
実はよくあるハマりなんですが、キューに入れたメッセージは処理後一定時間たつと再度見えるようになります。これを利用して

1.SQSにメッセージを投入する
2.Lambdaでメッセージ処理をする
    2.1 正常終了する
      -> キューからメッセージが消える
    2.2 異常終了する
      -> キューにメッセージが残る
3.異常終了した際は、メッセージが残るので一定時間たつと
  メッセージが再度見えるように(処理可能)になる
  ー> 2に戻る

お!?なんとなく行けそう?

ということで、今回はサンプル的に、リトライをするというところをメインに常に異常終了を起こすコードを書いて使い物になるか検証したいと思います。
実際の処理では、正常処理を実装し、条件分岐してリトライを実施したい場合のみエラーを起こします。

図にするとこんな感じです。

環境作成

設定値

AWS SQS

  • メッセージの配信遅延時間(DelaySeconds):10秒
  • キューからメッセージが配信された後にメッセージを利用できなくなる時間の長さ(VisibilityTimeout):60秒
  • Amazon SQS がメッセージを保持する秒数(MessageRetentionPeriod):5分(300秒)
  • 待機期間(ReceiveMessageWaitTimeSeconds):5秒

上記の設定でキューを作成します。 上手くいけば、メッセージを保持している5分間の間に4回実施されます。

CloudFormation

ここはさくっと、SQSとLambdaを作成します。
必要なリソースはCloudFormation一撃でできるように用意させて頂きました。
使用するテンプレートはこちら

AWSTemplateFormatVersion: '2010-09-09'
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "SQS Settings"
        Parameters:
          - DelaySeconds
          - ReceiveMessageWaitTimeSeconds
          - VisibilityTimeout
          - MessageRetentionPeriod
    ParameterLabels:
      DelaySeconds:
        default: "DelaySeconds"
      VisibilityTimeout:
        default: "VisibilityTimeout"
      ReceiveMessageWaitTimeSeconds:
        default: "ReceiveMessageWaitTimeSeconds"
      MessageRetentionPeriod:
        default: "MessageRetentionPeriod"
Parameters:
  DelaySeconds:
    Type: Number
    Default: 10
  VisibilityTimeout:
    Type: Number
    Default: 60
  ReceiveMessageWaitTimeSeconds:
    Type: Number
    Default: 5
  MessageRetentionPeriod:
    Type: Number
    Default: 300

Resources:
  LambdaRertryQueue:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: !Ref DelaySeconds
      ReceiveMessageWaitTimeSeconds: !Ref ReceiveMessageWaitTimeSeconds
      VisibilityTimeout: !Ref VisibilityTimeout
      MessageRetentionPeriod: !Ref MessageRetentionPeriod

  RetryLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          exports.handler = async (event) => {
              console.log(event);
              const error = new Error("retry Error");
              throw error;
          };
      Runtime: nodejs8.10
      Timeout: 30

  EventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      BatchSize: 1
      EventSourceArn: !GetAtt LambdaRertryQueue.Arn
      FunctionName: !GetAtt RetryLambdaFunction.Arn

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: arn:aws:logs:*:*:*
          - Effect: Allow
            Action:
              - sqs:DeleteMessage
              - sqs:GetQueueAttributes
              - sqs:ReceiveMessage
            Resource: arn:aws:sqs:*

※lamdaの権限はSQSのポーリングとCloudWatchLogsへの書き込みを許可しています。必要に応じて変更してください。

51~55行目でLambda関数を直書いてます。すべてエラーを返すようにしています。

テンプレートはS3に置いたので下のリンクをクリックして、テンプレートを実行してみてください。

ここをクリック

リトライするかやってみる

SQSにメッセージを投入する

SQSのAWSコンソールを開いて作成したキューに対して、 「キューの操作」「メッセージの送信」を行いメッセージをSQSに投入します (常にエラーをするようにしているので基本的にメッセージはなんでもいいです)

リトライの確認

CloudWatchのログを確認して、実行されているか確認します。

多少の誤差はありますが、無事に1分間隔で実施されているようです。また保持する秒数を5分にしているので、5分後メッセージは消えており無限リトライにもなっていないようです。 ご興味がある方がいたら、SQSの各種パラメータなどを変更して実施してみてください。

まとめ

関数内でループ処理を実装せずにループを行えるところがなかなか面白いです。 実際にプロダクションで使用する場合は、検証も含め以下の部分は注意してください。

  • 再処理可能になるまでの時間指定を間違えると、あっという間に数100?回リトライしますのでご注意ください。
  • 別のエラーが発生した場合に検出にしにくい。  こちらは、リトライのためのエラーと他のエラーが発生したときは区別するようにログ出力等などで対応する必要があります。
  • エラーを返さないでメッセージを再投入するなどの検討(SQSからのLambda実行でメッセージを残す方法などが整備されると嬉しいです!)

なかなか、おもしろい機構なのですが埋もれそうだったので抜粋して紹介してみました。 次回、この機構をつかって、実際に状態を監視するユースケースをご紹介しようと思います。

参考

https://dev.classmethod.jp/etc/aws-lambda-support-sqs-event-source/

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html