S3 イベント通知でLambdaを実行する構築ハンズオン

## はじめに
『S3にオブジェクトが作成されると、EventBridgeを利用して後続のStepFunctionsを起動して、、、』のような案件を経験して、完全に『S3からのイベント通知の際はEventBridgeだ！』という凝り固まった偏見を持っていました。
全然そんなことはなく、S3のイベント通知には`Lambda関数`,`SNSトピック`,`SQS キュー`の方法がありますので、凝り固まらないように『S3にオブジェクトが生成されるとイベント通知を行い、Lambdaが実行される』構築を行っていきます。

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

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

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

## ハンズオン
### 構築の流れ
#### 1.Lambda作成
#### 2.S3作成
#### 上記の順番で構築を行なっていきます。
最終的には、S3からのイベント通知を受けて、Lambdaのログ（CloudWatchLogs）が出力されれば検証
完了とします。
下記画像のように`BUCKET NAME`と`OBJECT NAME` 、`CONTENT TYPE`を取得して、ログに出力します。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-7.13.00-640x136.png)

-----------

### 1.Lambda作成
コード参考：[AWS Lambda デベロッパーガイド　　：　チュートリアル: Amazon S3 トリガーを使用して Lambda 関数を呼び出す](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html)
上記URLを参考にして、下記S3からのイベント通知の必要な情報を取得します。
今回は通知より、バケット名とオブジェクト名を取得しています。
ついでにバケット名とオブジェクト名を利用して、S3バケットのコンテンツタイプを取得もしています。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-7.20.51-600x640.png)

`Type: "AWS::Lambda::Permission"`を記述することで、S3からのイベント通知からLambdaをトリガーするようにしています。
下記記載方法だと`SourceArn`の対象が`arn:${AWS::Partition}:s3:::*`と、ワイルドカードを利用しているので、どのS3であっても通知を設定することでLambdaを呼び出せるような状態です（ワイルドカード部分を具体的なS3バケット名にすることで限定的にすることが可能です。）

```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 json
          import urllib.parse
          import boto3

          print('Loading function')

          s3 = boto3.client('s3')


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

              # Get the object from the event and show its content type
              try:
                  response = s3.get_object(Bucket=bucket, Key=key)
                  print("CONTENT TYPE: " + response['ContentType'])
                  return response['ContentType']
              except Exception as e:
                  print(e)
                  print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
                  raise e

      Description: !Ref Description
      FunctionName: !Ref FunctionName
      Handler: !Ref Handler 
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Timeout: !Ref Timeout
      Role: !GetAtt LambdaRole.Arn
      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:
                  - "s3:*" 
                Resource: !Sub "arn:${AWS::Partition}:s3:::*"

  TriggerLambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Lambda.Arn
      Principal: "s3.amazonaws.com"
      SourceArn: !Sub "arn:${AWS::Partition}:s3:::*"

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

### 2.S3作成
`NotificationConfiguration`部分でLambdaに対して通知する条件を記載しています。
今回通知するための条件として`prefix`が`infra`の場合、且つ`suffix`が`zip`の場合、`Function`として`1.Lambda作成`で構築したLambdaを呼び出します。

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:
        LambdaConfigurations:
          - Event: "s3:ObjectCreated:*"
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: !Ref Prefix
                  - Name: suffix
                    Value: !Ref Suffix
            Function: !ImportValue cfn-lmd-inamura-arn
      Tags:
        - Key: "User"
          Value: !Ref TagsName
# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: cfn-s3-BucketName
```

### 挙動の確認
#### ①S3の`イベント通知`を確認する
フィルターとして`infra`、`zip`が条件として付与されて、送信先も`Lambda`となっています。
`編集`を押下するこで、より詳細に（プレフィックス、サフィックスの確認、イベントタイプ等）
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-7.39.44-640x106.png)

#### ②`infra.zip`を保存する
フィルターで切り分けがされているかの確認もしてみました。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-7.56.36-1-640x224.png)

Lambdaのログストリームでの画面から、プレフィックス：`infra`で、サフィックス:`zip`の場合でないと、後続のLambdaに通知は行われていないようです。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-8.04.16-640x126.png)

#### ③LambdaのCloudWatchLogsを確認する
Lambdaのログに取得しようと考えていた`BUCKET NAME`と`OBJECT NAME` 、`CONTENT TYPE`が出力できました。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-25-7.47.49-640x317.png)

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

## さいごに
もとより多くない知識なので、『これをするには、これだ！』と決めつけず、ベストプラクティスや自分で考えて構築した無駄とも思える量を携えながら、構築できるものを増やしていきたいと思います。