使用 CloudFormation 在 S3 存储桶中创建 Lambda 通知

Posted

技术标签:

【中文标题】使用 CloudFormation 在 S3 存储桶中创建 Lambda 通知【英文标题】:Create a Lambda notification in an S3 bucket with CloudFormation 【发布时间】:2016-12-09 17:30:31 【问题描述】:

我正在尝试为 CloudFormation 模板中的 Lambda 函数创建 S3 触发器。 S3 存储桶已存在,正在创建 Lambda 函数。

This 表示无法使用 CFT 修改预先存在的基础架构(在本例中为 S3),但 this 似乎表示存储桶必须是预先存在的。

    似乎无法使用 CFT 类型“AWS::Lambda...”创建触发器,并且源服务需要创建触发器。就我而言,这是 s3 存储桶的 NotificationConfiguration-LambdaConfiguration。所有这些都正确吗?

    当我尝试使用 CFT 将 NotificationConfiguration 添加到现有 S3 存储桶时,它说我不能。有没有办法做到这一点?

【问题讨论】:

我相当肯定,虽然存储桶必须存在,但在创建模板之前它不必存在。在同一模板中与通知配置和 lambda 函数一起创建存储桶是否适合您的用例?如果是这样,这种方法将比修改现有基础架构更容易为您提供帮助。无论哪种方式都有一个解决方案,但一个更漂亮 当您说“S3 存储桶已存在”时,您是否还暗示该存储桶是在 CloudFormation 之外创建的? 【参考方案1】:

很遗憾,官方的AWS::CloudFormation模板只允许你控制Amazon S3 NotificationConfiguration作为父AWS::S3::Bucket资源的NotificationConfiguration property,这意味着你不能把这个配置附加到任何现有的bucket上,你必须将其应用到 CloudFormation 管理的存储桶以使其工作。

一种解决方法是使用putBucketNotificationConfiguration javascript API 调用将PUT Bucket Notification API 调用直接实现为Lambda-backed Custom Resource。但是,由于修改 S3 存储桶上的 NotificationConfiguration 仅限于存储桶的创建者,因此您还需要添加一个 AWS::S3::BucketPolicy 资源,以授予您的 Lambda 函数对 s3:PutBucketNotification 操作的访问权限。

这是一个完整的、自包含的 CloudFormation 模板,该模板演示了如何使用 2 个 Lambda 支持的自定义资源 (BucketConfiguration to设置存储桶通知配置,S3Object 将对象上传到存储桶)和第三个 Lambda 函数(BucketWatcher 用于在对象上传到存储桶时触发等待条件)。

Description: Upload an object to an S3 bucket, triggering a Lambda event, returning the object key as a Stack Output.
Parameters:
  Key:
    Description: S3 Object key
    Type: String
    Default: test
  Body:
    Description: S3 Object body content
    Type: String
    Default: TEST CONTENT
  BucketName:
    Description: S3 Bucket name (must already exist)
    Type: String
Resources:
  BucketConfiguration:
    Type: Custom::S3BucketConfiguration
    DependsOn:
    - BucketPermission
    - NotificationBucketPolicy
    Properties:
      ServiceToken: !GetAtt S3BucketConfiguration.Arn
      Bucket: !Ref BucketName
      NotificationConfiguration:
        LambdaFunctionConfigurations:
        - Events: ['s3:ObjectCreated:*']
          LambdaFunctionArn: !GetAtt BucketWatcher.Arn
  S3BucketConfiguration:
    Type: AWS::Lambda::Function
    Properties:
      Description: S3 Object Custom Resource
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var response = require('cfn-response');
          var AWS = require('aws-sdk');
          var s3 = new AWS.S3();
          exports.handler = function(event, context) 
            var respond = (e) => response.send(event, context, e ? response.FAILED : response.SUCCESS, e ? e : );
            process.on('uncaughtException', e=>failed(e));
            var params = event.ResourceProperties;
            delete params.ServiceToken;
            if (event.RequestType === 'Delete') 
              params.NotificationConfiguration = ;
              s3.putBucketNotificationConfiguration(params).promise()
                .then((data)=>respond())
                .catch((e)=>respond());
             else 
              s3.putBucketNotificationConfiguration(params).promise()
                .then((data)=>respond())
                .catch((e)=>respond(e));
            
          ;
      Timeout: 30
      Runtime: nodejs4.3
  BucketPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !Ref BucketWatcher
      Principal: s3.amazonaws.com
      SourceAccount: !Ref "AWS::AccountId"
      SourceArn: !Sub "arn:aws:s3:::$BucketName"
  BucketWatcher:
    Type: AWS::Lambda::Function
    Properties:
      Description: Sends a Wait Condition signal to Handle when invoked
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          exports.handler = function(event, context) 
            console.log("Request received:\n", JSON.stringify(event));
            var responseBody = JSON.stringify(
              "Status" : "SUCCESS",
              "UniqueId" : "Key",
              "Data" : event.Records[0].s3.object.key,
              "Reason" : ""
            );
            var https = require("https");
            var url = require("url");
            var parsedUrl = url.parse('$Handle');
            var options = 
                hostname: parsedUrl.hostname,
                port: 443,
                path: parsedUrl.path,
                method: "PUT",
                headers: 
                    "content-type": "",
                    "content-length": responseBody.length
                
            ;
            var request = https.request(options, function(response) 
                console.log("Status code: " + response.statusCode);
                console.log("Status message: " + response.statusMessage);
                context.done();
            );
            request.on("error", function(error) 
                console.log("send(..) failed executing https.request(..): " + error);
                context.done();
            );
            request.write(responseBody);
            request.end();
          ;
      Timeout: 30
      Runtime: nodejs4.3
  Handle:
    Type: AWS::CloudFormation::WaitConditionHandle
  Wait:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !Ref Handle
      Timeout: 300
  S3Object:
    Type: Custom::S3Object
    DependsOn: BucketConfiguration
    Properties:
      ServiceToken: !GetAtt S3ObjectFunction.Arn
      Bucket: !Ref BucketName
      Key: !Ref Key
      Body: !Ref Body
  S3ObjectFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: S3 Object Custom Resource
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var response = require('cfn-response');
          var AWS = require('aws-sdk');
          var s3 = new AWS.S3();
          exports.handler = function(event, context) 
            var respond = (e) => response.send(event, context, e ? response.FAILED : response.SUCCESS, e ? e : );
            var params = event.ResourceProperties;
            delete params.ServiceToken;
            if (event.RequestType == 'Create' || event.RequestType == 'Update') 
              s3.putObject(params).promise()
                .then((data)=>respond())
                .catch((e)=>respond(e));
             else if (event.RequestType == 'Delete') 
              delete params.Body;
              s3.deleteObject(params).promise()
                .then((data)=>respond())
                .catch((e)=>respond(e));
             else 
              respond(Error: 'Invalid request type');
            
          ;
      Timeout: 30
      Runtime: nodejs4.3
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal: Service: [lambda.amazonaws.com]
          Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
      - PolicyName: S3Policy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - 's3:PutObject'
                - 'S3:DeleteObject'
              Resource: !Sub "arn:aws:s3:::$BucketName/$Key"
  NotificationBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref BucketName
      PolicyDocument:
        Statement:
          - Effect: "Allow"
            Action:
            - 's3:PutBucketNotification'
            Resource: !Sub "arn:aws:s3:::$BucketName"
            Principal:
              AWS: !GetAtt LambdaExecutionRole.Arn
Outputs:
  Result:
    Value: !GetAtt Wait.Data

【讨论】:

使用 CloudFormation 在 AWS 中设置简单的东西需要多少代码真是太疯狂了 使用 CFN 通用资源提供程序(本质上是 boto3 代理)的类似解决方案:github.com/ab77/cfn-generic-custom-resource#s3【参考方案2】:

我使用另一种解决方案,因为我个人不喜欢自定义资源。我创建了一个云跟踪来捕获相关存储桶的 S3 写入事件,然后创建一个带有 S3 事件模式的 cloudwatch/eventbridge 规则来触发我的 Lambda 函数。通过这种方式,我可以将我的 Lambda 函数挂接到现有的 S3 存储桶而无需接触它,并且可以通过 cloudformation 轻松完成。

【讨论】:

请注意,这种方式不适用于跨账户 senario,因为我发现我的云跟踪无法捕获另一个账户上的 s3 事件。这可能是可行的,但我找不到简单的设置方法。

以上是关于使用 CloudFormation 在 S3 存储桶中创建 Lambda 通知的主要内容,如果未能解决你的问题,请参考以下文章

AWS Cloudformation 模板 - 在 S3 存储桶中设置区域

CloudFormation 模板设置 S3 存储桶默认加密 [重复]

如何使用 cloudformation 为 S3 存储桶设置半随机名称

AWS CloudFormation 更新 Lambda 代码以在 S3 存储桶中使用最新版本

使用 cloudformation yaml 在 Lambda 函数上添加 S3 触发器

无法在 cloudformation 中创建带有事件的 s3 存储桶,以连接到它