简体   繁体   中英

AWS CDK - How to add an event notification to an existing S3 Bucket

I'm trying to modify this AWS-provided CDK example to instead use an existing bucket. Additional documentation indicates that importing existing resources is supported. So far I am unable to add an event notification to the existing bucket using CDK.

Here is my modified version of the example:

class S3TriggerStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # create lambda function
        function = _lambda.Function(self, "lambda_function",
                                    runtime=_lambda.Runtime.PYTHON_3_7,
                                    handler="lambda-handler.main",
                                    code=_lambda.Code.asset("./lambda"))

        # **MODIFIED TO GET EXISTING BUCKET**
        #s3 = _s3.Bucket(self, "s3bucket")
        s3 = _s3.Bucket.from_bucket_arn(self, 's3_bucket',
            bucket_arn='arn:<my_region>:::<my_bucket>')

        # create s3 notification for lambda function
        notification = aws_s3_notifications.LambdaDestination(function)

        # assign notification for the s3 event type (ex: OBJECT_CREATED)
        s3.add_event_notification(_s3.EventType.OBJECT_CREATED, notification)

This results in the following error when trying to add_event_notification :

AttributeError: '_IBucketProxy' object has no attribute 'add_event_notification'

The from_bucket_arn function returns an IBucket , and the add_event_notification function is a method of the Bucket class, but I can't seem to find any other way to do this. Maybe it's not supported. Any help would be appreciated.

I managed to get this working with a custom resource . It's TypeScript, but it should be easily translated to Python:

const uploadBucket = s3.Bucket.fromBucketName(this, 'BucketByName', 'existing-bucket');

const fn = new lambda.Function(this, 'MyFunction', {
    runtime: lambda.Runtime.NODEJS_10_X,
    handler: 'index.handler',
    code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler'))
});

const rsrc = new AwsCustomResource(this, 'S3NotificationResource', {
    onCreate: {
        service: 'S3',
        action: 'putBucketNotificationConfiguration',
        parameters: {
            // This bucket must be in the same region you are deploying to
            Bucket: uploadBucket.bucketName,
            NotificationConfiguration: {
                LambdaFunctionConfigurations: [
                    {
                        Events: ['s3:ObjectCreated:*'],
                        LambdaFunctionArn: fn.functionArn,
                        Filter: {
                            Key: {
                                FilterRules: [{ Name: 'suffix', Value: 'csv' }]
                            }
                        }
                    }
                ]
            }
        },
        // Always update physical ID so function gets executed
        physicalResourceId: 'S3NotifCustomResource' + Date.now().toString()
    }
});

fn.addPermission('AllowS3Invocation', {
    action: 'lambda:InvokeFunction',
    principal: new iam.ServicePrincipal('s3.amazonaws.com'),
    sourceArn: uploadBucket.bucketArn
});

rsrc.node.addDependency(fn.permissionsNode.findChild('AllowS3Invocation'));

This is basically a CDK version of the CloudFormation template laid out in this example . See the docs on the AWS SDK for the possible NotificationConfiguration parameters.

Sorry I can't comment on the excellent James Irwin's answer above due to a low reputation, but I took and made it into a Construct .

The comment about "Access Denied" took me some time to figure out too, but the crux of it is that the function is S3:putBucketNotificationConfiguration , but the IAM Policy action to allow is S3:PutBucketNotification .

Here's the [code for the construct]:( https://gist.github.com/archisgore/0f098ae1d7d19fddc13d2f5a68f606ab )

import * as cr from '@aws-cdk/custom-resources';
import * as logs from '@aws-cdk/aws-logs';
import * as s3 from '@aws-cdk/aws-s3';
import * as sqs from '@aws-cdk/aws-sqs';
import * as iam from '@aws-cdk/aws-iam';
import {Construct} from '@aws-cdk/core';

// You can drop this construct anywhere, and in your stack, invoke it like this:
// const s3ToSQSNotification = new S3NotificationToSQSCustomResource(this, 's3ToSQSNotification', existingBucket, queue);

export class S3NotificationToSQSCustomResource extends Construct {

    constructor(scope: Construct, id: string, bucket: s3.IBucket, queue: sqs.Queue) {
        super(scope, id);

        // https://stackoverflow.com/questions/58087772/aws-cdk-how-to-add-an-event-notification-to-an-existing-s3-bucket
        const notificationResource = new cr.AwsCustomResource(scope, id+"CustomResource", {
            onCreate: {
                service: 'S3',
                action: 'putBucketNotificationConfiguration',
                parameters: {
                    // This bucket must be in the same region you are deploying to
                    Bucket: bucket.bucketName,
                    NotificationConfiguration: {
                        QueueConfigurations: [
                            {
                                Events: ['s3:ObjectCreated:*'],
                                QueueArn: queue.queueArn,
                            }
                        ]
                    }
                },
                physicalResourceId: <cr.PhysicalResourceId>(id + Date.now().toString()),
            },
            onDelete: {
                service: 'S3',
                action: 'putBucketNotificationConfiguration',
                parameters: {
                    // This bucket must be in the same region you are deploying to
                    Bucket: bucket.bucketName,
                    // deleting a notification configuration involves setting it to empty.
                    NotificationConfiguration: {
                    }
                },
                physicalResourceId: <cr.PhysicalResourceId>(id + Date.now().toString()),
            },
            policy: cr.AwsCustomResourcePolicy.fromStatements([new iam.PolicyStatement({
                // The actual function is PutBucketNotificationConfiguration.
                // The "Action" for IAM policies is PutBucketNotification.
                // https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html#amazons3-actions-as-permissions
                actions: ["S3:PutBucketNotification"],
                 // allow this custom resource to modify this bucket
                resources: [bucket.bucketArn],
            })]),
            logRetention: logs.RetentionDays.ONE_DAY,
        });

        // allow S3 to send notifications to our queue
        // https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#grant-destinations-permissions-to-s3
        queue.addToResourcePolicy(new iam.PolicyStatement({
            principals: [new iam.ServicePrincipal("s3.amazonaws.com")],
            actions: ["SQS:SendMessage"],
            resources: [queue.queueArn],
            conditions: {
                ArnEquals: {"aws:SourceArn": bucket.bucketArn}
            }
        }));

        // don't create the notification custom-resource until after both the bucket and queue
        // are fully created and policies applied.
        notificationResource.node.addDependency(bucket);
        notificationResource.node.addDependency(queue);
    }
}

since June 2021 there is a nicer way to solve this problem. Since approx. Version 1.110.0 of the CDK it is possible to use the S3 notifications with Typescript Code:

Example:

const s3Bucket = s3.Bucket.fromBucketName(this, 'bucketId', 'bucketName');
s3Bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(lambdaFunction), {
    prefix: 'example/file.txt'
});

CDK Documentation: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-notifications-readme.html

Pull Request: https://github.com/aws/aws-cdk/pull/15158

UPDATED: Source code from original answer will overwrite existing notification list for bucket which will make it impossible adding new lambda triggers. Here's the solution which uses event sources to handle mentioned problem.

import aws_cdk {
    aws_s3 as s3,
    aws_cdk.aws_lambda as lambda_
    aws_lambda_event_sources as event_src
}
import path as path

class S3LambdaTrigger(core.Stack):
    
    def __init__(self, scope: core.Construct, id: str):
        
        super().__init__(scope, id)
        
        bucket = s3.Bucket(
            self, "S3Bucket",
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
            bucket_name='BucketName',
            encryption=s3.BucketEncryption.S3_MANAGED,
            versioned=True
        )

        fn = lambda_.Function(
            self, "LambdaFunction",
            runtime=lambda_.Runtime.NODEJS_10_X,
            handler="index.handler",
            code=lambda_.Code.from_asset(path.join(__dirname, "lambda-handler"))
        )

        fn.add_permission(
            's3-service-principal', 
            principal=aws_iam.ServicePrincipal('s3.amazonaws.com')
        )

        fn.add_event_source(
            event_src.S3EventSource(
                bucket, 
                events=[s3.EventType.OBJECT_CREATED, s3.EventType.OBJECT_REMOVED],
                filters=[s3.NotificationKeyFilter(prefix="subdir/", suffix=".txt")]
            )
        )

ORIGINAL: I took ubi 's solution in TypeScript and successfully translated it to Python. His solution worked for me.

#!/usr/bin/env python

from typing import List

from aws_cdk import (
    core,
    custom_resources as cr,
    aws_lambda as lambda_,
    aws_s3 as s3,
    aws_iam as iam,
)


class S3NotificationLambdaProps:
    def __init__(self, bucket: s3.Bucket, function: lambda_.Function, events: List[str], prefix: str):
        self.bucket = bucket
        self.function = function
        self.events = events
        self.prefix = prefix


class S3NotificationLambda(core.Construct):

    def __init__(self, scope: core.Construct, id: str, props: S3NotificationLambdaProps):
        super().__init__(scope, id)

        self.notificationResource = cr.AwsCustomResource(
            self, f'CustomResource{id}',
            on_create=cr.AwsSdkCall(
                service="S3",
                action="S3:putBucketNotificationConfiguration",
                # Always update physical ID so function gets executed
                physical_resource_id=cr.PhysicalResourceId.of(f'S3NotifCustomResource{id}'),
                parameters={
                    "Bucket": props.bucket.bucket_name,
                    "NotificationConfiguration": {
                        "LambdaFunctionConfigurations": [{
                            "Events": props.events,
                            "LambdaFunctionArn": props.function.function_arn,
                            "Filter": {
                                "Key": {"FilterRules": [{"Name": "prefix", "Value": props.prefix}]}
                            }}
                        ]
                    }
                }
            ),
            on_delete=cr.AwsSdkCall(
                service="S3",
                action="S3:putBucketNotificationConfiguration",
                # Always update physical ID so function gets executed
                physical_resource_id=cr.PhysicalResourceId.of(f'S3NotifCustomResource{id}'),
                parameters={
                    "Bucket": props.bucket.bucket_name,
                    "NotificationConfiguration": {},
                }
            ),
            policy=cr.AwsCustomResourcePolicy.from_statements(
                statements=[
                    iam.PolicyStatement(
                        actions=["S3:PutBucketNotification", "S3:GetBucketNotification"],
                        resources=[props.bucket.bucket_arn]
                    ),
                ]
            )
        )

        props.function.add_permission(
            "AllowS3Invocation",
            action="lambda:InvokeFunction",
            principal=iam.ServicePrincipal("s3.amazonaws.com"),
            source_arn=props.bucket.bucket_arn,
        )

        # don't create the notification custom-resource until after both the bucket and lambda
        # are fully created and policies applied.
        self.notificationResource.node.add_dependency(props.bucket)
        self.notificationResource.node.add_dependency(props.function)
# Usage:

s3NotificationLambdaProps = S3NotificationLambdaProps(
    bucket=bucket_,
    function=lambda_fn_,
    events=['s3:ObjectCreated:*'],
    prefix='foo/'
)

s3NotificationLambda = S3NotificationLambda(
    self, "S3NotifLambda",
    self.s3NotificationLambdaProps
)

Here is a python solution for adding / replacing a lambda trigger to an existing bucket including the filter. @James Irwin your example was very helpful.
Thanks to @JørgenFrøland for pointing out that the custom resource config will replace any existing notification triggers based on the boto3 documentation https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.BucketNotification.put

One note is he access denied issue is because if you do putBucketNotificationConfiguration action the policy creates a s3:PutBucketNotificationConfiguration action but that action doesn't exist https://github.com/aws/aws-cdk/issues/3318#issuecomment-584737465 Same issue happens if you set the policy using AwsCustomResourcePolicy.fromSdkCalls I've added a custom policy that might need to be restricted further.

s3_bucket = s3.Bucket.from_bucket_name(
    self, 's3-bucket-by-name', 'existing-bucket-name')

trigger_lambda = _lambda.Function(
    self,
    '{id}-s3-trigger-lambda',
    environment=lambda_env,
    code=_lambda.Code.from_asset('./ladle-sink/'),
    runtime=_lambda.Runtime.PYTHON_3_7,
    handler='lambda_function.lambda_handler',
    memory_size=512,
    timeout=core.Duration.minutes(3))

trigger_lambda.add_permission(
    's3-trigger-lambda-s3-invoke-function',
    principal=iam.ServicePrincipal('s3.amazonaws.com'),
    action='lambda:InvokeFunction',
    source_arn=base_resources.incoming_documents_bucket.bucket_arn)

custom_s3_resource = _custom_resources.AwsCustomResource(
    self,
    's3-incoming-documents-notification-resource',
    policy=_custom_resources.AwsCustomResourcePolicy.from_statements([
        iam.PolicyStatement(
            effect=iam.Effect.ALLOW,
            resources=['*'],
            actions=['s3:PutBucketNotification']
        )
    ]),
    on_create=_custom_resources.AwsSdkCall(
        service="S3",
        action="putBucketNotificationConfiguration",
        parameters={
            "Bucket": s3_bucket.bucket_name,
            "NotificationConfiguration": {
                "LambdaFunctionConfigurations": [
                    {
                        "Events": ['s3:ObjectCreated:*'],
                        "LambdaFunctionArn": trigger_lambda.function_arn,
                        "Filter": {
                            "Key": {
                                "FilterRules": [
                                    {'Name': 'suffix', 'Value': 'html'}]
                            }
                        }
                    }
                ]
            }
        },
        physical_resource_id=_custom_resources.PhysicalResourceId.of(
            f's3-notification-resource-{str(uuid.uuid1())}'),
        region=env.region
    ))

custom_s3_resource.node.add_dependency(
    trigger_lambda.permissions_node.find_child(
        's3-trigger-lambda-s3-invoke-function'))

Thanks to the great answers above, see below for a construct for s3 -> lambda notification. It can be used like

    const fn = new SingletonFunction(this, "Function", {
    ...
    });

    const bucket = Bucket.fromBucketName(this, "Bucket", "...");

    const s3notification = new S3NotificationLambda(this, "S3Notification", {
      bucket: bucket,
      lambda: function,
      events: ['s3:ObjectCreated:*'],
      prefix: "some_prefix/"
    })

Construct (drop-in to your project as a .ts file)

import * as cr from "@aws-cdk/custom-resources";
import * as logs from "@aws-cdk/aws-logs";
import * as s3 from "@aws-cdk/aws-s3";
import * as sqs from "@aws-cdk/aws-sqs";
import * as iam from "@aws-cdk/aws-iam";
import { Construct } from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";

export interface S3NotificationLambdaProps {
  bucket: s3.IBucket;
  lambda: lambda.IFunction;
  events: string[];
  prefix: string;
}

export class S3NotificationLambda extends Construct {
  constructor(scope: Construct, id: string, props: S3NotificationLambdaProps) {
    super(scope, id);

    const notificationResource = new cr.AwsCustomResource(
      scope,
      id + "CustomResource",
      {
        onCreate: {
          service: "S3",
          action: "putBucketNotificationConfiguration",
          parameters: {
            // This bucket must be in the same region you are deploying to
            Bucket: props.bucket.bucketName,
            NotificationConfiguration: {
              LambdaFunctionConfigurations: [
                {
                  Events: props.events,
                  LambdaFunctionArn: props.lambda.functionArn,
                  Filter: {
                    Key: {
                      FilterRules: [{ Name: "prefix", Value: props.prefix }],
                    },
                  },
                },
              ],
            },
          },
          physicalResourceId: <cr.PhysicalResourceId>(
            (id + Date.now().toString())
          ),
        },
        onDelete: {
          service: "S3",
          action: "putBucketNotificationConfiguration",
          parameters: {
            // This bucket must be in the same region you are deploying to
            Bucket: props.bucket.bucketName,
            // deleting a notification configuration involves setting it to empty.
            NotificationConfiguration: {},
          },
          physicalResourceId: <cr.PhysicalResourceId>(
            (id + Date.now().toString())
          ),
        },
        policy: cr.AwsCustomResourcePolicy.fromStatements([
          new iam.PolicyStatement({
            // The actual function is PutBucketNotificationConfiguration.
            // The "Action" for IAM policies is PutBucketNotification.
            // https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html#amazons3-actions-as-permissions
            actions: ["S3:PutBucketNotification", "S3:GetBucketNotification"],
            // allow this custom resource to modify this bucket
            resources: [props.bucket.bucketArn],
          }),
        ]),
      }
    );

    props.lambda.addPermission("AllowS3Invocation", {
      action: "lambda:InvokeFunction",
      principal: new iam.ServicePrincipal("s3.amazonaws.com"),
      sourceArn: props.bucket.bucketArn,
    });

    // don't create the notification custom-resource until after both the bucket and queue
    // are fully created and policies applied.
    notificationResource.node.addDependency(props.bucket);
    notificationResource.node.addDependency(props.lambda);
  }
}

based on the answer from @ubi

in case of you don't need the SingletonFunction but Function + some cleanup

call like this:

const s3NotificationLambdaProps = < S3NotificationLambdaProps > {
    bucket: bucket,
    lambda: lambda,
    events: ['s3:ObjectCreated:*'],
    prefix: '', // or put some prefix
};

const s3NotificationLambda = new S3NotificationLambda(this, `${envNameUpperCase}S3ToLambdaNotification`, s3NotificationLambdaProps);

and the construct will be like this:

import * as cr from "@aws-cdk/custom-resources";
import * as s3 from "@aws-cdk/aws-s3";
import * as iam from "@aws-cdk/aws-iam";
import { Construct } from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";

export interface S3NotificationLambdaProps {
    bucket: s3.IBucket;
    lambda: lambda.Function;
    events: string[];
    prefix: string;
}

export class S3NotificationLambda extends Construct {
    constructor(scope: Construct, id: string, props: S3NotificationLambdaProps) {
        super(scope, id);

        const notificationResource = new cr.AwsCustomResource(
            scope,
            id + "CustomResource", {
                onCreate: {
                    service: "S3",
                    action: "putBucketNotificationConfiguration",
                    parameters: {
                        // This bucket must be in the same region you are deploying to
                        Bucket: props.bucket.bucketName,
                        NotificationConfiguration: {
                            LambdaFunctionConfigurations: [{
                                Events: props.events,
                                LambdaFunctionArn: props.lambda.functionArn,
                                Filter: {
                                    Key: {
                                        FilterRules: [{
                                            Name: "prefix",
                                            Value: props.prefix
                                        }],
                                    },
                                },
                            }, ],
                        },
                    },
                    physicalResourceId: < cr.PhysicalResourceId > (
                        (id + Date.now().toString())
                    ),
                },
                onDelete: {
                    service: "S3",
                    action: "putBucketNotificationConfiguration",
                    parameters: {
                        // This bucket must be in the same region you are deploying to
                        Bucket: props.bucket.bucketName,
                        // deleting a notification configuration involves setting it to empty.
                        NotificationConfiguration: {},
                    },
                    physicalResourceId: < cr.PhysicalResourceId > (
                        (id + Date.now().toString())
                    ),
                },
                policy: cr.AwsCustomResourcePolicy.fromStatements([
                    new iam.PolicyStatement({
                        // The actual function is PutBucketNotificationConfiguration.
                        // The "Action" for IAM policies is PutBucketNotification.
                        // https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html#amazons3-actions-as-permissions
                        actions: ["S3:PutBucketNotification", "S3:GetBucketNotification"],
                        // allow this custom resource to modify this bucket
                        resources: [props.bucket.bucketArn],
                    }),
                ]),
            }
        );

        props.lambda.addPermission("AllowS3Invocation", {
            action: "lambda:InvokeFunction",
            principal: new iam.ServicePrincipal("s3.amazonaws.com"),
            sourceArn: props.bucket.bucketArn,
        });

        // don't create the notification custom-resource until after both the bucket and lambda
        // are fully created and policies applied.
        notificationResource.node.addDependency(props.bucket);
        notificationResource.node.addDependency(props.lambda);
    }
}

I used CloudTrail for resolving the issue, code looks like below and its more abstract:

const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail');

const options: AddEventSelectorOptions = {
  readWriteType: cloudtrail.ReadWriteType.WRITE_ONLY
};

// Adds an event selector to the bucket 
trail.addS3EventSelector([{
  bucket: bucket, // 'Bucket' is of type s3.IBucket,
}], options);

bucket.onCloudTrailWriteObject('MyAmazingCloudTrail', {
  target: new targets.LambdaFunction(functionReference)
});

This is CDK solution.

  1. Get a grab of existing bucket using fromBucketAttributes
  2. Then for your bucket, use addEventNotification to trigger your lambda.
declare const myLambda: lambda.Function;
const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', {
  bucketArn: 'arn:aws:s3:::my-bucket',
});

// now you can just call methods on the bucket
bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(myLambda), {prefix: 'home/myusername/*'});

More details can be found here

AWS now supports s3 eventbridge events, which allows for adding a source s3 bucket by name. So this worked for me. Note that you need to enable eventbridge events manually for the triggering s3 bucket.

  new Rule(this, 's3rule', {
                   eventPattern: {
                       source: ['aws.s3'],
                       detail: {
                           'bucket': {'name': ['existing-bucket']},
                           'object': {'key' : [{'prefix' : 'prefix'}]}
                       },
                       detailType: ['Object Created']
                   },
                   targets: [new targets.LambdaFunction(MyFunction)]
               }
           );

With the newer functionality, in python this can now be done as:

bucket = aws_s3.Bucket.from_bucket_name(
    self, "bucket", "bucket-name"
)


bucket.add_event_notification(
    aws_s3.EventType.OBJECT_CREATED,
    aws_s3_notifications.LambdaDestination(your_lambda),
    aws_s3.NotificationKeyFilter(
        prefix="prefix/path/",
    ),
)

At the time of writing, the AWS documentation seems to have the prefix arguments incorrect in their examples so this was moderately confusing to figure out.

Thanks to @Kilian Pfeifer for starting me down the right path with the typescript example.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM