S3オブジェクトをトリガーにしたLambdaからSNS通知構築ハンズオン

## はじめに
今回もCloudFormationを利用して、S3にオブジェクトを保存するとEventBridgeが検知をして後続のLambdaをターゲットにし、LambdaからSNSを自分宛に送信するような構築をしていきます。
S3からオブジェクト通知でSNSという経路もありますが、自分のリソース理解の意味も含め構築をしていきます。

------------

## 構成図
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-15.16.11-640x199.png)

------------

## ハンズオン
### 構築の流れ
#### 1.S3作成
#### 2.SNS作成
#### 3.Lambda作成
#### 4.EventBridge作成
上記の順番で構築を行なっていきます。

最終的にS3にオブジェクトを保存すると、SNSで登録した宛先にメールが届きます。
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.05.10-640x132.png)


### 1.S3作成
10回ほどS3にオブジェクトを保存してEventBridgeに通知されないことを不審に思い、公式ドキュメントを再読。
`NotificationConfiguration`の記述をすることで、後続の`EventBridge`に通知を送ることが出来るようになることを知りました。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation to create S3 Bucket
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 Configuration"
        Parameters:
        - S3BucketName
        - AccessControl
        - BlockPublicAcls
        - BlockPublicPolicy
        - IgnorePublicAcls
        - RestrictPublicBuckets
        - ExpirationInDays
        - EventBridgeConfiguration
# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  S3BucketName:
    Type: String
    Default: "cfn-s3-XXXXXXXX"
    Description: Type of this BacketName.
  AccessControl:
    Type: String
    Description: AccessControl.
    Default: "Private"
    AllowedValues: [ "Private", "PublicRead", "PublicReadWrite", "AuthenticatedRead", "LogDeliveryWrite", "BucketOwnerRead", "BucketOwnerFullControl", "AwsExecRead" ]
  BlockPublicAcls: 
    Type: String
    Description: BlockPublicAcls.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  BlockPublicPolicy:
    Type: String
    Description: BlockPublicPolicy.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  IgnorePublicAcls:
    Type: String
    Description: IgnorePublicAcls.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  RestrictPublicBuckets:
    Type: String
    Description: RestrictPublicBuckets.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  ExpirationInDays:
    Type: String
    Description: Lifecycle Days.
    Default: "7"
  EventBridgeConfiguration:
    Type: String
    Description: EventBridgeConfiguration.
    Default: "true"
    AllowedValues: [ "true", "false" ]

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
      AccessControl: !Ref AccessControl
      PublicAccessBlockConfiguration:
        BlockPublicAcls: !Ref BlockPublicAcls
        BlockPublicPolicy: !Ref BlockPublicPolicy
        IgnorePublicAcls: !Ref IgnorePublicAcls
        RestrictPublicBuckets: !Ref RestrictPublicBuckets
      LifecycleConfiguration:
        Rules:
          - Id: LifeCycleRule
            Status: Enabled
            ExpirationInDays: !Ref ExpirationInDays
      NotificationConfiguration:
        EventBridgeConfiguration: 
          EventBridgeEnabled: !Ref EventBridgeConfiguration

# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: !Ref S3BucketName
```

###　2.SNS作成
#### 2.1 CloudFormationでSNSトピック及びサブスクリプションを作成する
自身のメールアドレスにSNSから通知がるように設定をします
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: SNS Create

# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "SNS Configuration"
        Parameters:
        - TopicName
        - Endpoint

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  TopicName:
    Type: String
    Default: "cfn-sns-topic-inamura"
  Endpoint:
    Type: String
    Default: "XXXXXXXXXX@gmail.com"


# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Ref TopicName
      Subscription:
        - Endpoint: !Ref Endpoint
          Protocol: email

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  LambdaArn:
    Value: !Ref SNSTopic
    Export:
      Name: !Sub "${TopicName}-arn"
```
#### 2.2 上記設定後に、SNSから送られてきたメールのサブスクリプションを押下する
①送られてきたメールの赤枠部分をクリックする
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.13.42-640x157.png)

②画面が遷移して下記画面が表示されると、サブスクリプションが開始される
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.15.17.png)
※上記青枠部分をクリックすると、サブスクリプションが解除される


### 3.Lambda作成
EventBridgeをトリガーにして起動するLambdaです。
通知内容を構成して、CDK(boto3)を利用してSNSでメールを送信します。

20回程度動かして連続して同じ内容を送信できないことを知りました。
その解決方法として件名部分に`datetime`を利用してユニークにしています。
今思えば折角くEventBridgeからの通知を受け取っているので、アップロード時間やオブジェクト名など、もっとスマートなやり方はあると思いますが、今回は検証ということで簡易な方法で済ましています。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  Lambda Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Lambda Configuration"
        Parameters:
        - FunctionName
        - Description
        - Handler
        - MemorySize
        - Runtime
        - Timeout

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  FunctionName:
    Type: String
    Default: "cfn-lmd-inamura"
  Description:
    Type: String
    Default: "cfn-lmd-inamura"
  Handler:
    Type: String
    Default: "index.lambda_handler"
  MemorySize:
    Type: String
    Default: "128"
  Runtime:
    Type: String
    Default: "python3.9"
  Timeout:
    Type: String
    Default: "180"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  Lambda
# ------------------------------------------------------------#
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os
          import datetime

          client = boto3.client('sns')
          SNS = os.environ['SNS']
          Subject = "【件名】SNS通知 "
          Message = "S3にオブジェクトが作成されました"

          def lambda_handler(event, context):
              date = datetime.datetime.now()
              d = date.strftime('%Y%m%d %H:%M:%S')

              params = {
              'TopicArn': SNS,
              'Subject': Subject + str(d),
              'Message': Message
              }
              
              client.publish(**params)

      Description: !Ref Description
      FunctionName: !Ref FunctionName
      Handler: !Ref Handler 
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Timeout: !Ref Timeout
      Environment:
        Variables:
          SNS: !ImportValue cfn-sns-topic-inamura-arn
          TZ: "Asia/Tokyo"
      Role: !GetAtt LambdaRole.Arn

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${FunctionName}-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: !Sub "${FunctionName}-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
				  - "logs:CreateLogGroup"
                Resource:  !Sub "arn:${AWS::Partition}:logs:*:*:*"

              - Effect: "Allow"
                Action:
                  - "sns:Publish"
                Resource:  "*"

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  LambdaArn:
    Value: !GetAtt Lambda.Arn
    Export:
      Name: !Sub "${FunctionName}-arn"
```

### 4.EventBridge作成
S3にオブジェクトが作成されたことをトリガーにして、ターゲットをLambdaにしているEventBridgeです。

30回程度S3にオブジェクトを作成した後、` Type: AWS::Lambda::Permission`が記載されていないことに気がつきました。
記載することで`EventBridge`から`Lambda`に対してトリガーを設定することが出来ました。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  EventBridge gets s3 events and sends them to lambda
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Eventbridge Configuration"
        Parameters:
          - Name
          - EventBusName
          - State
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  EventBusName:
    Type: String
    Default: "default"
  Name:
    Type: String
    Default: "cfn-SendLambdaS3Events-inamura"
  State:
    Type: String
    Default: "ENABLED"

# ------------------------------------------------------------#
#  EventBridge
# ------------------------------------------------------------#
Resources:
  S3EventsRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Get S3 events and send to Lambda"
      EventBusName: !Ref EventBusName
      Name: !Ref Name
      State: !Ref State
      EventPattern:
        source: 
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name: 
              - !ImportValue cfn-s3-XXXXXXXX
      Targets:
        - Arn: !ImportValue cfn-lmd-inamura-arn
          Id: "cfn-lmd-inamura"
  
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: cfn-lmd-inamura
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt 'S3EventsRule.Arn'
```

### 挙動の確認
#### ①構築した`S3`にオブジェクトを保存（アップロード）する
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.36.41-627x640.png)

#### ②SNSでサブスクリプションを開始したメールアドレスに通知がくる
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.05.10-1-640x132.png)

#### ③EventBridgeのメトリクスを確認する
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.41.01-640x216.png)

#### ④Lambdaのログを確認して【ERROR】が出力されていない
![](https://cloud5.jp/wp-content/uploads/2022/11/スクリーンショット-2022-11-26-14.51.54-640x34.png)

------------

## さいごに
知らないため設定をしていなかったり、理解が浅くて設定を間違っていたなどの問題も多かったですが、最終的に自分の想定したような動きを構築することが出来て概ね満足しました。
ただし途中でも書いたように、`EventBridge`を利用しているのだから、`Date`ではなくS3にアップロードした時間を利用して件名をつけるなど、もう少しスマートに書けた部分は多かったと、誰に有益なのか分からない反省もしています。
まだまだ構築していきたいものは多いので、手を動かして知見を増やしていければと思います。