CloudFormation 一撃で既存のS3バケットでAWS LambdaのS3のオブジェクト作成通知を追加作成してみた

2021.08.06

AWS事業本部 梶原@福岡オフィスです。

AWSのブログで公開されている 'CloudFormation を使用して、既存の S3 バケットで Lambda 用の Amazon S3 通知設定を作成する方法を教えてください。 の内容の拡張になります。

参照元のブログの重要事項であげられているとおり

重要: 次の手順は、通知設定が作成されていない S3 バケットでの S3 通知設定にのみ適用されます。S3 バケットに通知設定が既に存在するか、手動で作成された通知設定がある場合、次の手順を実行すると、それらの設定は上書きされます。

既存バケットに通知設定が何もない場合にはよいのですが、すでに設定されている通知設定は、上書き消去。上書き消去!!!されます

カスタムリソース処理自体が不安なようでしたら、CloudFormationのインポートを使用した方法もあります。

AWS CloudFormation リソースのインポートを使用して、既存の S3 バケットで AWS Lambda の Amazon S3 通知設定を作成するにはどうすればよいですか?

こちらの方法でもCloudFormationのインポートを用いて設定することは可能ですが、インポート時に設定を揃える必要があり、こちらも既存の設定は消えることになるのご注意ください

ということで、カスタムリソース部分を変更したので共有します。 Lambda関数部分(オブジェクト作成時に起動するLambdaはご自身の関数に置き換えてください

こちらのCloudFormationテンプレートを使用して(Lambda部分は変更しています)、Redshiftのログに自動でタグをつける方法はこちらのブログでご案内しています。

説明

実際のテンプレートは最後に記載しています。

パラメータ部

Parameters:
  NotificationBucket:
    Type: String
    Description: S3 bucket that's used for the Lambda event notification
  Prefix:
    Type: String
    Description: Prefix of the object key name for filtering rules
  Suffix:
    Type: String
    Description: Suffix of the object key name for filtering rules

NotificationBucket: オブジェクト作成時にLamba関数を呼び出す既存のバケットになります

Prefix: Lamba関数を呼び出す際にフィルタリングを行うオブジェクトの接頭辞(Path)をパラメータで渡します

Suffix: Lamba関数を呼び出す際にフィルタリングを行うオブジェクトの接尾辞(拡張子など)をパラメータで渡します

Lambda関数(オブジェクト作成時に呼び出される)

  S3NotificationLambdaFunction:
    Type: 'AWS::Lambda::Function'
  省略

特になにもしていないので、ご自身の関数に置き換えてください

Lambda用のロール

  LambdaIAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:GetBucketNotification'
                  - 's3:PutBucketNotification'
                Resource: !Sub 'arn:aws:s3:::${NotificationBucket}'
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: 'arn:aws:logs:*:*:*'

オブジェクト作成時に呼び出されるLambda, カスタムリソースLambdaと共通で使っています。 実際に使用する際は適切なロールを別々に割り当ててください

カスタムリソース用にs3:GetBucketNotification, s3:PutBucketNotification の許可を付与しています

AWS Lambdaアクセス許可

  LambdaInvokePermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      FunctionName: !GetAtt S3NotificationLambdaFunction.Arn
      Action: 'lambda:InvokeFunction'
      Principal: s3.amazonaws.com
      SourceAccount: !Ref 'AWS::AccountId'
      SourceArn: !Sub 'arn:aws:s3:::${NotificationBucket}'

S3のイベントからLambdaを起動する際にはLambda関数に対してS3からのアクセス許可(起動許可)が必要となります。 イベントが発火しない(Labmda)場合は、この設定が抜けている事が多いです

カスタムリソース

  LambdaTrigger:
    Type: 'Custom::LambdaTrigger'
    DependsOn: LambdaInvokePermission
    Properties:
      ServiceToken: !GetAtt CustomResourceLambdaFunction.Arn
      Id: !Sub
        - S3LambdaNotif-${UniqueId}
        - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]
      Bucket: !Ref NotificationBucket
      Prefix: !Ref Prefix
      Suffix: !Ref Suffix
      LambdaArn: !GetAtt S3NotificationLambdaFunction.Arn

カスタムリソースLambdaをパラメータ付きで呼び出します。 Idは、CloudFormationスタックのIDを使用してユニークな文字列を作成しています。

S3のイベント通知設定に追加するLambda関数

  CustomResourceLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambda_handler
      Role: !GetAtt LambdaIAMRole.Arn
      Code:
 コード部省略

Pythonで以下の処理を実装しています。

リソース作成、更新時

  1. 既存のS3のLambda通知設定を取得する
  2. Lambda通知設定を追加します(ID, Prefix, Suffix)を条件に追加、更新
  3. S3の通知設定追加処理を呼び出します。

リソース削除時

  1. 既存のS3のLambda通知設定を取得する
  2. Lambda通知設定を削除(IDで削除)
  3. S3の通知設定追加処理を呼び出します。

S3の通知設定追加処理

  1. 既存の通知(キュー、Topic)があれば、呼び出し条件に追加します
  2. S3の通知設定を更新します

スタックの作成

パラメータを指定して、通常通りスタックを作成します。

一応、設定が上書きされても問題ないS3を指定して検証してみてください。

既存のイベント設定がある場合には追加、スタックの削除で作成したイベントのみが削除されるかと思います。

Prefix, Suffixが既存のイベントと重複する場合は作成が失敗しますので、ご注意ください

Template

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  Sample template to illustrate use of existing S3 bucket as an event source for a Lambda function
Parameters:
  NotificationBucket:
    Type: String
    Description: S3 bucket that's used for the Lambda event notification
  Prefix:
    Type: String
    Description: Prefix of the object key name for filtering rules
  Suffix:
    Type: String
    Description: Suffix of the object key name for filtering rules

Resources:
  S3NotificationLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import json
          def lambda_handler(event,context):
              return 'Welcome... This is a test Lambda Function'
      Handler: index.lambda_handler
      Role: !GetAtt LambdaIAMRole.Arn
      Runtime: python3.8
      Timeout: 5

  LambdaIAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:GetBucketNotification'
                  - 's3:PutBucketNotification'
                Resource: !Sub 'arn:aws:s3:::${NotificationBucket}'
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: 'arn:aws:logs:*:*:*'

  LambdaInvokePermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      FunctionName: !GetAtt S3NotificationLambdaFunction.Arn
      Action: 'lambda:InvokeFunction'
      Principal: s3.amazonaws.com
      SourceAccount: !Ref 'AWS::AccountId'
      SourceArn: !Sub 'arn:aws:s3:::${NotificationBucket}'

  LambdaTrigger:
    Type: 'Custom::LambdaTrigger'
    DependsOn: LambdaInvokePermission
    Properties:
      ServiceToken: !GetAtt CustomResourceLambdaFunction.Arn
      Id: !Sub
        - S3LambdaNotif-${UniqueId}
        - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]
      Bucket: !Ref NotificationBucket
      Prefix: !Ref Prefix
      Suffix: !Ref Suffix
      LambdaArn: !GetAtt S3NotificationLambdaFunction.Arn

  CustomResourceLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambda_handler
      Role: !GetAtt LambdaIAMRole.Arn
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse

          SUCCESS = "SUCCESS"
          FAILED = "FAILED"

          print('Loading function')
          s3 = boto3.resource('s3')

          def lambda_handler(event, context):
              print("Received event: " + json.dumps(event, indent=2))
              responseData={}
              try:
                  if event['RequestType'] == 'Delete':
                      print("Request Type:",event['RequestType'])
                      Id=event['ResourceProperties']['Id']
                      Bucket=event['ResourceProperties']['Bucket']

                      delete_notification(Id, Bucket)
                      print("Sending response to custom resource after Delete")
                  elif event['RequestType'] == 'Create' or event['RequestType'] == 'Update':
                      print("Request Type:",event['RequestType'])
                      Id=event['ResourceProperties']['Id']
                      Prefix=event['ResourceProperties']['Prefix']
                      Suffix=event['ResourceProperties']['Suffix']
                      LambdaArn=event['ResourceProperties']['LambdaArn']
                      Bucket=event['ResourceProperties']['Bucket']
                      
                      add_notification(Id, Prefix, Suffix, LambdaArn, Bucket)
                      responseData={'Bucket':Bucket}
                      print("Sending response to custom resource")
                  responseStatus = 'SUCCESS'
              except Exception as e:
                  print('Failed to process:', e)
                  responseStatus = 'FAILED'
                  responseData = {'Failure': 'Something bad happened.'}
              cfnresponse.send(event, context, responseStatus, responseData)

          def add_notification(Id, Prefix, Suffix, LambdaArn, Bucket):
              bucket_notification = s3.BucketNotification(Bucket)
              print(bucket_notification.lambda_function_configurations)

              lambda_function_configurations = bucket_notification.lambda_function_configurations

              if lambda_function_configurations is None:
                  lambda_function_configurations = []
              else:
                  lambda_function_configurations = [e for e in lambda_function_configurations if e['Id'] != Id]

              lambda_config = {}
              lambda_config['Id'] = Id
              lambda_config['LambdaFunctionArn'] = LambdaArn
              lambda_config['Events'] = ['s3:ObjectCreated:*']
              lambda_config['Filter'] = {'Key': {'FilterRules': [
                      {'Name': 'Prefix', 'Value': Prefix},
                      {'Name': 'Suffix', 'Value': Suffix}
                      ]}
              }
              
              lambda_function_configurations.append(lambda_config)
              print(lambda_function_configurations)
              
              put_bucket_notification(bucket_notification, lambda_function_configurations)
              
              print("Put request completed....")
            
          def delete_notification(Id, Bucket):

              bucket_notification = s3.BucketNotification(Bucket)
              print(bucket_notification.lambda_function_configurations)

              lambda_function_configurations = bucket_notification.lambda_function_configurations

              if lambda_function_configurations is not None:
                  lambda_function_configurations = [e for e in lambda_function_configurations if e['Id'] != Id]

              print(lambda_function_configurations)

              put_bucket_notification(bucket_notification, lambda_function_configurations)

              print("Delete request completed....")

          def put_bucket_notification(BucketNotification, LambdaFunctionConfigurations):

              notification_configuration = {}
              if LambdaFunctionConfigurations is not None:
                  notification_configuration['LambdaFunctionConfigurations'] = LambdaFunctionConfigurations
              
              if BucketNotification.queue_configurations is not None:
                  notification_configuration['QueueConfigurations'] = BucketNotification.queue_configurations

              if BucketNotification.topic_configurations is not None:
                  notification_configuration['TopicConfigurations'] = BucketNotification.topic_configurations

              print(notification_configuration)
              
              response = BucketNotification.put(
                NotificationConfiguration= notification_configuration
              )
      Runtime: python3.8
      Timeout: 50

参考

AWS CloudFormation リソースのインポートを使用して、既存の S3 バケットで AWS Lambda の Amazon S3 通知設定を作成するにはどうすればよいですか?
https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-s3-notification-config/

Amazon S3 コンソールを使用したイベント通知の有効化と設定
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/enable-event-notifications.html

CloudFormation を使用して、既存の S3 バケットで Lambda 用の Amazon S3 通知設定を作成する方法を教えてください。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-s3-notification-lambda/

S3にcreateObjectをトリガーにLambdaを起動するCloudformationテンプレート
https://dev.classmethod.jp/articles/cloudformation-s3-put-event/