DynamoDB StreamsのイベントをフィルタリングしてLambdaを実行する（おまけ付き）

# はじめに
　Lambdaでは、DynamoDB Streamsでデータの変更を検知して実行することができます。以前は、変更のたびに毎回Lambdaが実行され、Lambdaのコード内で追加、更新、削除を判断して処理を行う必要がありました。この方法では、毎回Lambdaが実行されるため、その分、費用がかかる状態でした。
　それが2021年11月のアップデートでLambda実行前に処理を実行するかどうかフィルタリングすることができるようになり、コードの簡略化や不要な実行がなくなり費用も低減できます。
　今回は、実際にフィルタリングを設定し、実行してみます。

# 事前準備
　トリガーとなるDynamoDBのテーブルを作成し、ストリームを有効化しておきます。その後、ストリームARNをコピーしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/8da1e5c4-7959-3a56-75b4-defebfb66c2a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/52830093-184c-2e95-3c7b-4bfe82b61e91.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/5dda07c2-0309-b5c6-73f9-7fd57422a73b.png)

# Lambda Functionを作成する
　今回は、pythonでログを出力するだけの処理で確認します。実際には、DynamoDBのテーブルにレコードが追加されたら、「管理者、ユーザに通知する」、「分析用のテーブルにレコードを追加する」等のユースケースで利用することがあるかと思います。

## 基本設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/31d30997-4782-1ec6-f7f6-2b4b43687c66.png)

## Lambda Functionのソース

```python:index.py
import json

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event))
    for record in event['Records']:
        print(record['eventID'])
        print(record['eventName'])
        print("DynamoDB Record: " + json.dumps(record['dynamodb']))
    return 'Successfully processed {} records.'.format(len(event['Records']))

```

#　アクセス権限
　Lambda Functionのアクセス権限には以下の通り、DynamoDB Streamsへのアクセス権限が必要なります。※ポリシー抜粋

```json:ロールに付与されたポリシー
{
    "Action": [
        "dynamodb:GetRecords",
        "dynamodb:GetShardIterator",
        "dynamodb:DescribeStream",
        "dynamodb:ListStreams"
    ],
    "Effect": "Allow",
    "Resource": [
        "arn:aws:dynamodb:[リージョン]:[アカウントID]:table/[テーブル名]/stream/[ストリームID]"
    ]
}

```

# トリガー
　トリガーにDynamoDBを設定します。フィルタリング条件については後述します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/7c872fdc-c22c-6665-1861-6927b9eff659.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/51ce0db5-fca4-1fa6-b33b-71307dde20d7.png)

## フィルタリング条件の設定
　トリガー設定にあるフィルタリング条件を追加し、Lambda Functionを実行したい条件を設定します。

### 「追加」の場合に実行する

```json:insert
{
  "eventName": ["INSERT"]
}
```

### 「更新」の場合に実行する

```json:update
{
  "eventName": ["UPDATE"]
}
```

### 「削除」の場合に実行する

```json:delete
{
  "eventName": ["DELETE"]
}
```

細かいフィルタールールについては、以下を参考すると良いです。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/invocation-eventfiltering.html#filtering-basics

## トリガーを確認する
　設定したトリガーを確認します。追加したフィルタリング条件が設定されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/05f25b9d-48fd-4835-400f-c30134fa5ba4.png)


:::note warn
ご注意
作成された以下のフィルタリング条件をそのままコピーして作成しようとするとエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/cb50b996-39c3-f0fa-1968-ca56b54e248a.png)

↓エラー。恥ずかしながらハマってました。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/3cf1bad9-e39a-c7d4-10e2-697b77c87fa7.png)

↓正解はこっちです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/9138bcc2-99b6-b14b-b1fc-091bb25b4189.png)
:::


# 動きを確認する
　では、実際に動かしてみます。以下が既存のテーブルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/3eb92834-50fc-e9f4-299f-bf32d733bd6e.png)

レコードを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/73b691d6-cbed-0362-6d06-8fef9b1f0903.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/5519b1bd-aa72-895d-1480-8c4cb952690e.png)

Lambda Functionのログが以下の通り、出力されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/b07d24c9-b3c7-66d1-f1ee-40a738548e49.png)


次にレコードの削除を行なってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/bf7c2550-e6ea-1fdd-5b35-eb83fd556d49.png)

Lambda Functionのログが以下の通り、出力されていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/18b408b6-dc10-adb3-f235-432439fa4e7d.png)

メトリクスも見てみます。Invocationsのカウントが1です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129814/d8b548ca-9d69-3250-e2dd-62f40d0fc3e9.png)


# おまけ（CLoudFormationテンプレート）
　ここまで手動でやってきましたが、やはり手早く使いたい方もいるかと思いますので、CloudFormationテンプレートを載せておきます。レコードが追加された場合に実行されるサンプルになります。

```yaml:lambda-filtering.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Filtering event sources for AWS Lambda functions'

# Parameters
Parameters:

  DynamoDBStreamsArn:
    Type: String
    MinLength: 1

# Resources
Resources:

  # Role
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-execution-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      Path: /
      Policies:
        - PolicyName: lambda-execution-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource:
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-*'
              - Action:
                  - dynamodb:GetRecords
                  - dynamodb:GetShardIterator
                  - dynamodb:DescribeStream
                  - dynamodb:ListStreams
                Effect: Allow
                Resource:
                  - !Ref DynamoDBStreamsArn

  # Lambda
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties: 
      Architectures: 
        - arm64
      Code:
        ZipFile: |
          import json

          def lambda_handler(event, context):
              print("Received event: " + json.dumps(event))
              for record in event['Records']:
                  print(record['eventID'])
                  print(record['eventName'])
                  print("DynamoDB Record: " + json.dumps(record['dynamodb']))
              return 'Successfully processed {} records.'.format(len(event['Records']))
      FunctionName: !Sub '${AWS::StackName}-function'
      Handler: index.lambda_handler
      MemorySize: 128
      Role: !GetAtt ExecutionRole.Arn
      Runtime: python3.9
      Timeout: 300

  # Lambda - EventSourceMapping
  EventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties: 
      BatchSize: 100
      BisectBatchOnFunctionError: false
      Enabled: true
      EventSourceArn: !Ref DynamoDBStreamsArn
      FilterCriteria: 
        Filters: 
          -  Pattern: "{\"eventName\": [\"INSERT\"]}"
      FunctionName: !Ref LambdaFunction
      MaximumBatchingWindowInSeconds: 0
      MaximumRecordAgeInSeconds: -1
      MaximumRetryAttempts: -1
      ParallelizationFactor: 1
      StartingPosition: LATEST
      TumblingWindowInSeconds: 0
```

# まとめ
　フィルタリングの設定を行うことでフィルタリングするコードが不要となり、簡略化され、さらに費用が少なくなるのは良いですね。おまけ含めて、ぜひご活用いただければと思います。良いものは使いましょー