S3 イベント通知でSQSとLambdaを経由してSNS送信する構築ハンズオン

## はじめに
前回[S3 イベント通知でLambdaを実行する構築ハンズオン](https://cloud5.jp/execute-lambda-in-s3-event/)で、S3イベント通知で同期的にLambdaを呼び出しをしてみましたが、実際には非同期で処理をすることの方が多いかもと思いまして、`S3 →　SQS　→　Lambda → SNS`という構築のハンズオンしていきます。

・`S3 → EventBridge → Lambda` 呼び出しのパターン
[S3オブジェクトをトリガーにした zip展開するLambda構築ハンズオン](https://cloud5.jp/deployment-lambda-trigger-s3/)

・`S3 → Lambda` 呼び出しのパターン
[S3 イベント通知でLambdaを実行する構築ハンズオン](https://cloud5.jp/execute-lambda-in-s3-event/)


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

## 構成図
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-30-15.08.41-640x189.png)


### 挙動について
#### 1.S3 に プレフィックス`infra`で、サフィックス`zip`のオブジェクトを保存する
#### 2.上記の条件を満たしている場合、SQSへイベント通知が行われる
#### 3.SQSをトリガーにLambdaがSQSのキューを取得し、内容からSNSへメールを成形して送信する
【注意】Lambdaが失敗しても、SQSからのキューが再度送られるので再度Lambdaが呼び出されます。構築は挙動の確認までを1セットで検証ください。
[10日間 で AWS Lambda 関数を 28億回 実行した話](https://blog.mmmcorp.co.jp/blog/2019/12/25/lambda-cloud-bankruptcy/)
[LambdaのリトライをAWS SQSを使ってやってみる](https://dev.classmethod.jp/articles/lambda-sqs-retry/)
#### 4.登録したメールアドレスに、Lambdaで成形された内容が通知される

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

## ハンズオン
### 構築のながれ
#### 1.SNS作成：登録したメールアドレスに、S3に保存したオブジェクト名・バケット名を送信する
#### 2.SQS作成：特定のオブジェクトが保存された場合、キューを作成する
#### 3.S3作成：オブジェクトを保存する、特定の条件の場合にイベント通知をおこうなう
#### 4.Lambda作成：SQSからキューを取得し、キューの情報からメール内容を生成して、メール送信をする


-----------

### 1.SNS作成：登録したメールアドレスに、S3に保存したオブジェクト名・バケット名を送信する
##### 1.1 SNSを構築する
Configで非準拠判定された場合に通知する、SNSトピックを構築します。
`TopicPolicy`部分は、検証のため制限していません。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: SNS Create

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

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

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Ref TopicName
      Subscription:
        - Endpoint: !Ref Endpoint
          Protocol: email
      Tags:
        - Key: "User"
          Value: !Ref TagsValueUserName    

  TopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref SNSTopic
      PolicyDocument:
        Id: !Ref SNSTopic
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS: "*"
            Action: SNS:Publish
            Resource: !Ref SNSTopic

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  SNSArn:
    Value: !Ref SNSTopic
    Export:
      Name: !Sub "${TopicName}-arn"
  SNSTopicName:
    Value: !Ref TopicName
    Export:
      Name: !Ref TopicName
```

#### 1.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)
※上記青枠部分をクリックすると、サブスクリプションが解除される

### 2.SQS作成：特定のオブジェクトが保存された場合、キューを作成する
S3のイベント通知を受領するSQSを構築します。
イベント通知の際に利用できるキューは`FIFO`は対応していないため`標準`での構築です。
`FIFO`を利用することを考えたら、`S3 →　Lambda　→ SQS(FIFO) → Lambda`などの構成になるかと思います。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: SQS Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "SQS Configuration"
        Parameters:
        - QueueName
        - TagsValue

    ParameterLabels:
      QueueName:
        default: "QueueName"
      TagsValue:
        default: "TagsValue"

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  QueueName:
    Type: String
    Default: "cfn-sqs-inamura"
  TagsValue:
    Type: String
    Default: "inamura"

# ------------------------------------------------------------#
#  SQS
# ------------------------------------------------------------#
Resources:
  NotifySQS:
    Type: AWS::SQS::Queue
    Properties: 
      QueueName: !Ref QueueName
      Tags: 
        - Key: "User"
          Value: !Ref TagsValue

# ------------------------------------------------------------#
#  SQS QueuePolicy
# ------------------------------------------------------------#    
  SQSPolicy: 
    Type: AWS::SQS::QueuePolicy
    Properties: 
      Queues: 
        - !Ref NotifySQS
      PolicyDocument: 
        Statement: 
          - 
            Action: 
              - "SQS:*"
            Effect: "Allow"
            Resource: !GetAtt NotifySQS.Arn
            Principal:  
              AWS: 
                - "*"  

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  NotifySQS:
    Value: !GetAtt NotifySQS.QueueName
    Export:
      Name: !Sub "${QueueName}-queuename"
  QueueArn:
    Value: !GetAtt NotifySQS.Arn
    Export:
      Name: !Sub "${QueueName}-queuearn"
```

### 3.S3作成：オブジェクトを保存する、特定の条件の場合にイベント通知をおこうなう
`NotificationConfiguration`部分でSQSに対して通知する条件を記載しています。
今回通知するための条件として`prefix`が`infra`の場合、且つ`suffix`が`zip`の場合、`SQS`として`2.SQS作成`で構築したSQSを呼び出します。

S3のバケット名は全世界でユニークのため、`cfn-s3-20221217-inamura`部分は各自修正ください
※現在既にcfn-s3-20221217-inamuraは削除されております
```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
        - Prefix
        - Suffix
        - TagsName

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  S3BucketName:
    Type: String
    Default: "cfn-s3-20221217-inamura"
    Description: Type of this BacketName.
  VersioningConfiguration:
    Type: String
    Default: "Enabled"
    Description: VersioningConfiguration.
  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"
  Prefix:
    Type: String
    Description: Lambdafunction Trigger Prefix.
    Default: "infra"
  Suffix:
    Type: String
    Description: Lambdafunction Trigger Suffix
    Default: "zip"
  TagsName:
    Type: String
    Description: UserName
    Default: "inamura"
  
# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName      
      VersioningConfiguration:
        Status: !Ref VersioningConfiguration
      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:
        QueueConfigurations: 
          - Event: "s3:ObjectCreated:*"
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: !Ref Prefix
                  - Name: suffix
                    Value: !Ref Suffix
            Queue: !ImportValue cfn-sqs-inamura-queuearn
      Tags:
        - Key: "User"
          Value: !Ref TagsName
# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: cfn-s3-BucketName
```

### 4.Lambda作成：SQSからキューを取得し、キューの情報からメール内容を生成して、メール送信をする
`Type: "AWS::Lambda::EventSourceMapping"`部分に`SQS`からの通知を取得しにいくLambdaのトリガーとなる部分を記載します。
`EventBridge`などでは`Type: "AWS::Lambda::Permission"`でLambdaに他リソースからの許可を与えていましたが、`SQS`に関しては`SQS`に対してLambdaが通知を取得しにいくため記載の方法が異なります。
そのためLambdaにアタッチするRoleに、`SQS`に対してのメッセージを取得を許可する下記記載の部分があります。



```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
        - TagsName

# ------------------------------------------------------------#
#  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: "10"
  TagsName:
    Type: String
    Description: UserName
    Default: "inamura"
# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  Lambda
# ------------------------------------------------------------#
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os
          import datetime
          import urllib.parse
          import json

          print('Loading function')

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

          def lambda_handler(event, context):
              for record in event['Records']:
                  payload = record["body"]
                  events = json.loads(payload)

                  bucket = urllib.parse.unquote_plus(events['Records'][0]['s3']['bucket']['name'], encoding='utf-8')
                  key = urllib.parse.unquote_plus(events['Records'][0]['s3']['object']['key'], encoding='utf-8')
                  
                  print("BUCKET NAME:" + str(bucket))
                  print("OBJECT NAME:" + str(key))

          
                  date = datetime.datetime.now()
                  d = date.strftime('%Y%m%d %H:%M:%S')

                  params = {
                  'TopicArn': SNS,
                  'Subject': Subject + str(d),
                  'Message': Message + "\n\n" + "S3バケット名   :" + str(bucket) + "\n" + "オブジェクト名:" + str(key)
                  }
              
                  sns_client.publish(**params)

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

  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: "*"

              - Effect: Allow
                Action:
                  - sqs:DeleteMessage
                  - sqs:GetQueueAttributes
                  - sqs:ReceiveMessage
                Resource:
                  - !ImportValue cfn-sqs-inamura-queuearn

  EventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties: 
      Enabled: true
      EventSourceArn: !ImportValue cfn-sqs-inamura-queuearn
      FunctionName: !GetAtt Lambda.Arn
      BatchSize: 1

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

### 挙動の確認
#### ①S3に プレフィックス`infra`　で　サフィックス `zip` を満たしているオブジェクトを保存する
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-30-15.44.44.png)

#### ②`SNS`で設定したメールアドレスにメールが送信される
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-30-15.47.23.png)

#### ③LambdaのCloudWatchLogsを確認する
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-30-15.48.50-640x304.png)

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

## さいごに
Lambdaのトリガーをつくるのは`Type: "AWS::Lambda::Permission"`だと思い込んでいたところがあり、GUIで「LambdaのトリガーにSQSが反映されないなぁ。」なんて30分くらい待ちぼうけていたのは、AWSでの背後の動きがわかっていないことが如実にわかりました。
それとSQSからLambdaに対しての通知の受け取りも、逐一受け取ったEventから型を見ながら、なんとか取得できました。きっとスマートなやり方が世界にはあるはず。もっと勉強が必要だなぁと作ることを通して学ばされます。