DynamoDB Streamの表示タイプをCloud Formationで変更する際に大ハマリした話

こんにちは、CX事業本部の若槻です。

今回は、システムで稼働中のDynamoDB Streamの表示タイプをCloud Formationで「NEW_IMAGE」から「NEW_AND_OLD_IMAGES」に変更した際に大ハマリしてしまったので、その際に発生したこと、原因、行った対処について整理してみました。

前提(機能追加前の構成)

あるAWSシステムで「DynamoDBのテーブルにデータが作成されたらDynamoDB StreamによりLambdaが起動して作成されたデータをもとに処理を行う」機能が実装されていました。

前提となる開発方針として、AWSリソースをCloud Formationでデプロイする際のスタックはリソース種類ごとに分けているため、Cloud Formationのテンプレートファイルも以下のようにDynamoDBテーブルとLambda Functionで分けて作成しています。

Resources:
  DemoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: demo_table
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      StreamSpecification:
        StreamViewType: NEW_IMAGE

Outputs:
  DemoDDBStream:
    Value: !GetAtt DemoTable.StreamArn
    Export:
      Name: demoTableStream
Resources:
  DemoFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: demo_func
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              ## 行いたい処理
              return
      Handler: index.lambda_handler
      MemorySize: 128
      Timeout: 30
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/Lambda_Basic_Role
      Runtime: python3.7

  EventMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      EventSourceArn: !ImportValue demoTableStream
      FunctionName: !Ref DemoFunction
      StartingPosition: LATEST

発生したこと

当初はテーブルでのデータの作成時にLambdaで新しいデータを処理するのみだったので、DynamoDB Streamの表示タイプは「NEW_IMAGE」で十分でした。

しかし、その後開発が進み、システムでの機能追加によりテーブルでのデータの変更時にLambda側で新旧のデータを比較する処理が必要となりました。

そこで、下記のようにdynamodb.ymlでDynamoDBテーブルリソースのStreamViewTypeの値を「NEW_IMAGE」から「NEW_AND_OLD_IMAGES」に変更し、既存のスタックに対してアップデートを行おうとしました。

Resources:
  DemoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: demo_table
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES

Outputs:
  DemoDDBStream:
    Value: !GetAtt DemoTable.StreamArn
    Export:
      Name: demoTableStream
  • DynamoDBのアップデートコマンド
$ aws cloudformation update-stack \
    --stack-name demo-dynamodb \
    --template-body file://dynamodb.yml

すると、下記のようにスタックのアップデートが失敗し、ROLLBACKが行われてしまいました。 image.png

DynamoDBテーブルのアップデート自体はできていますが、その後にExport demoTableStream cannot be updated as it is in use by demo-lambdaという理由でDynamoDB StreamのArnのExportが失敗しているようです。

原因と切り戻し対処

原因は、DynamoDB Streamの表示タイプを更新するとストリームのArnが更新されるため、Lambdaのトリガーにすでに設定されているストリームのArnとの不整合が起きたことでした。

そして、表示タイプの更新が一度でも行われると、以下画像の通りストリームのArnは更新前の値には戻らず、Cloud FormationのROLLBACKが行われた後も不整合状態のままとなるため、Lambda側でDynamoDB Streamのトリガーを再作成することにより切り戻しを行う必要があります。

  • LambdaのコンソールよりLambdaのトリガーを見ると、ストリームのArnは末尾2020-02-12T03:26:15.334である。(値はトリガー作成時から更新されていない) image.png

  • DynamoDBのコンソールよりDynamoDB Streamの詳細を見ると、ストリームの表示タイプは「新しいイメージ」のままだが、Arnは末尾2020-02-12T13:52:37.889の値に更新されている。 image.png

この状態のままではLambdaが動作しないため、以下の切り戻し対処を行いました。

  1. lambda.ymlでEventSourceMappingリソースの記述を削除し、スタックをアップデートする。
  2. dynamodb.ymlでDynamoDBリソースのStreamViewTypeの値を「NEW_IMAGE」に戻し、DynamoDB StreamのArnのOutputの記述を削除して、スタックをアップデートする。
  3. dynamodb.ymlでDynamoDB StreamのArnのOutputの記述を追加して、スタックをアップデートする。
  4. lambda.ymlでEventSourceMappingリソースの記述を追加してスタックをアップデートする。

これでデプロイ作業前の状態にシステムを切り戻すことができました。

なお、切り戻し対処時に合わせて表示タイプを「NEW_AND_OLD_IMAGES」に変更することも可能でしたが、復旧優先およびデプロイ方法の仕切り直したのめここでは一旦「NEW_IMAGE」に戻す対応を行っています。

恒久対処(「NEW_AND_OLD_IMAGES」への変更)

表示タイプの「NEW_AND_OLD_IMAGES」への変更は、DynamoDB StreamのArnをSSM Parameterを利用してdynamodb.ymlのスタックからlambda.ymlのスタックに渡す方法を取ることとしました。(Export/ImportによるArnの受け渡しをやめた)また、既存の処理に影響が出ないよう以下のように2回のリリースに分けてデプロイを行う方針としました。

  • 次回のリリース
  1. dynamodb.ymlでDynamoDB StreamのArnを値に持つSSM Parameterのリソースを追加し、スタックをアップデートする。
  2. lambda.ymlでEventSourceMappingリソースのトリガーとなるArnに1. で作成したSSM Parameterの値を利用するようにし、スタックをアップデートする。
  • 次々回のリリース
  1. dynamodb.ymlでDynamoDBリソースのStreamViewTypeの値を「NEW_AND_OLD_IMAGES」に変更し、スタックをアップデートする。
  2. lambda.ymlのスタックをデプロイし、「NEW_AND_OLD_IMAGES」による処理を行うLambda Functionに更新する。

なお、ここでも「Lambdaからトリガーを削除し、DynamoDB Streamの表示タイプを「NEW_AND_OLD_IMAGES」に変更してから、再度Lambdaのトリガーを作成する」という方法も考えられますが、CircleCIによるデプロイ自動化を行っているなどの都合により今回はSSM Parameterによる方法を採用しています。

根本的な回避策

システムを構築する早い段階で以下いずれかの方針を取っていれば今回のトラブルは未然に回避できていたと思われます。

  • DynamoDBテーブルとそのストリームをトリガーとして起動されるLambdaは別々のスタックでリソースを作成せず、単一のスタックで作成する。
  • DynamoDB Streamを利用する際は表示タイプは初回構築時に「NEW_AND_OLD_IMAGES」とする

おわりに

今回の経験によりDynamoDB Streamを実装する際の注意点を把握することができました。どのようにリソースをデプロイするのが最適であるかは、実現したいシステムやプロジェクトによって異なりますし、どの方法にも一長一短があると思うので、臨機応変に立ち向かっていく必要がありますね。

以上