Trouble with Snapshot tests in CDK - amazon-web-services

I am trying to do some snapshot testing on my CDK stack but the snapshot is not generating.
This is my stack:
export interface SNSStackProps extends cdk.StackProps {
assumedRole: string
}
export class SNSStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: AssumedRole) {
super(scope, id, props)
const topicName = "TopicName"
const topic = new sns.Topic(this, topicName, {
displayName: "Topic Name",
fifo: true,
topicName: topicName,
contentBasedDeduplication: true
})
const assumedRole = iam.Role.fromRoleArn(
this,
"AssumedRole",
props.assumedRole
)
topic.grantPublish(assumedRole.grantPrincipal)
}
}
This is my snapshot test
test("Creates an SNS topic ", () => {
const stack = new Stack()
new SNSStack.SNSStack(stack, "SNSStack", {
env: {
account: "test_account",
region: "test_region"
},
assumedRoleArn: "arn:aws:iam::1111111:role/testRole"
})
expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot()
})
This generates a snapshot with an empty object like this
exports[`dlq creates an alarm 1`] = `Object {}`;
Why is the object empty in the snapshot? And how do I get the Object in the snapshot to populate with the resources in my stack?

You should create a Stack by using an App and not another stack. You can then easily synthesize the App and extract the JSON cloudformation stack which you can use for your snapshot. Below is an example of how I've used it to create the stack and retrieve the CloudFormation.
const app = new App();
new ApiStack(app, 'api-stack', params);
return app.synth({ force: true }).getStackByName('api-stack').template;
It is possible that once you've got the stack reference, you can use the SynthUtils way to get the CloudFormation repository.

Related

Error in AWS CDK V2 construct for AWS ECR

I have written code to create a repo and a few properties. Even though I am passing reponame as a string 'testing' as part of an interface, my code is going through the else condition and creating the reponame as undefined+date.
2nd issue: Can you also help me to find the issue for the principal in the permission policy? I am receiving an error saying props.accountIds.map is wrong: I am passing an array to accountIds.
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { Repository, RepositoryEncryption, TagMutability } from 'aws-cdk-lib/aws-ecr';
import {AWSAccountDetails} from '../lib/utils/definition';
import * as cdk from 'aws-cdk-lib';
export class ecrStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: any ){
super(scope, id);
const repository = this.createEcr(props);
this.createAdditionalProperty(repository,props);
}
//Method to check and create the AWS ECR REPO
private createEcr( props: AWSAccountDetails): any {
let imageTagMutability : ecr.TagMutability = ecr.TagMutability.IMMUTABLE;
let imageScanOnPush : Boolean =true;
let encryption : ecr.RepositoryEncryption =ecr.RepositoryEncryption.KMS;
if ( props.imageTagMutability in ecr.TagMutability ) {
imageTagMutability =props.imageTagMutability;
}
if (typeof props.imageScanOnPush ! == 'boolean'){
imageScanOnPush =props.imageScanOnPush;
}
if (typeof props.encryption ! == 'undefined'){
encryption =props.encryption;
}
if (!props.repositoryName) {
throw Error('No repository name provided');
}
let repository = ecr.Repository.fromRepositoryName(this, 'ecrRepo', props.repositoryName);
if (!repository.repositoryArn) {
// Repository does not exist, create a new one with the original name
repository=new ecr.Repository(this, props.repositoryName, {
repositoryName: props.repositoryName,
imageTagMutability: props.imageTagMutability,
encryption: RepositoryEncryption.KMS,
imageScanOnPush: props.imageScanOnPush,
removalPolicy: RemovalPolicy.DESTROY
});
} else {
const modifiedRepositoryName = `${props.repositoryName}-${Date.now()}`;
repository= new ecr.Repository(this, modifiedRepositoryName, {
repositoryName: modifiedRepositoryName,
imageTagMutability: props.imageTagMutability,
encryption: RepositoryEncryption.KMS,
imageScanOnPush: props.imageScanOnPush,
removalPolicy: RemovalPolicy.DESTROY
});
}return repository;
}
//Method to add the lifecycle policy,Tags and create aws account permissions.
private createAdditionalProperty(repository: any, props:AWSAccountDetails) {
let AgeOfImage :number =180;
if (typeof props.ImageAge ! == 'undefined'){
repository.addLifecycleRule({
rulePriority: 1,
maxImageAge:Duration.days(AgeOfImage)
});
} else {
repository.addLifecycleRule({
rulePriority: 1,
maxImageAge:Duration.days(props.ImageAge)
});
}
//Tags
const Tags:{[key:string]:string}={
Name: props.repositoryName,
}
//Permission to external aws account to grant permission for ECR pull and push
// const policy = new iam.PolicyDocument();
//policy.addStatements(new iam.PolicyStatement({
// actions: ['ecr:*'],
//actions: ['ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', 'ecr:BatchGetImage', 'ecr:PutImage']
// resources: [repository.repositoryArn],
// principals: props.accountIds.map(id => new iam.AccountPrincipal(id))
// }));
}
addLifecycleRule(arg0: { rulePriority: number; maxImageAge: Duration; }) {
throw new Error('Method not implemented.');
}
}
The interface file:
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { ecrStack } from '../ecrstack-stack';
export interface AWSAccountDetails {
ImageCount: any;
readonly repositoryName :'abcd'; /* Repo Name */
readonly ImageAge:110; //Number of days before image is deleted.i.e 90. need to change to imageAge
readonly imageTagMutability : ecr.TagMutability.IMMUTABLE; /* If the Repo should enable Tag Immutability or not; Default setting is Enabled */
readonly imageScanOnPush : true; /* If the Repo should enable ScanonPush or not ; Default setting is Enabled */
readonly encryption : 'KMS'; /* If the Repo should KMS or not ; Default setting is Enabled for AWS managed KMS Key*/
readonly accountIds : string //Account number to grant access to pull and push.
readonly encruptionproperty: 'KMS';
}
I have to pass the props as object and then export to the main stack. This solved the issue.

Cdk watch expects an environment variable to be a string while it is already a string

This error occasionally occurs on "cdk watch" and disappears when I destroy and redeploy the stack. All the global and per lambda variables are strings for sure. The table name is not declared explicitly but generated from the id..(maybe this is the cause of the issue?)
export class MyStack extends Stack {
constructor(app: App, id: string, props: MyStackProps) {
super(app, id);
const isProd = props.deploymentEnv;
const stackName = Stack.of(this).stackName;
const PRIMARY_KEY = 'reportId';
const dynamoTable = new Table(this, `MyTable-${stackName}`, {
partitionKey: {
name: PRIMARY_KEY,
type: AttributeType.STRING,
},
stream: StreamViewType.NEW_IMAGE,
removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
});
// Default props for lambda functions
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
'#sparticuz/chrome-aws-lambda',
],
},
depsLockFilePath: join(__dirname, '../package-lock.json'),
environment: {
PRIMARY_KEY: PRIMARY_KEY,
TABLE_NAME: dynamoTable.tableName,
},
runtime: Runtime.NODEJS_16_X,
};
In the lambda file, I'm getting the variables this way:
const TABLE_NAME = process.env.TABLE_NAME ?? '';
The error:
failed: InvalidParameterType: Expected params.Environment.Variables['TABLE_NAME'] to
be a string

CDK to enable DNS resolution for VPCPeering

I have VPC peering to connect to a lambda in one aws account to a RDS instance in another aws account. This works fine but required the VPC peering to have DNS resolution option enabled.
By default DNS resolution is set to :
DNS resolution from accepter VPC to private IP :Disabled.
This can be done via the AWS console and the CLI. I am not able to achieve the same using AWS CDK.
https://docs.aws.amazon.com/vpc/latest/peering/modify-peering-connections.html
The CfnVPCPeeringConnection does not seem to have this option.
https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-ec2.CfnVPCPeeringConnection.html
Is there any other way of achieving this via CDK ?
const cfnVPCPeeringConnection :CfnVPCPeeringConnection =
new CfnVPCPeeringConnection(
stack,
"vpcPeeringId",
{
peerVpcId : "<vpcId of acceptor account>",
vpcId : "<reference of the Id>",
peerOwnerId : "<aws acc number>",
peerRegion : "<region>",
peerRoleArn :"<arn created in the acceptor account>"",
}
);
//update route tables
rdsConnectorVpc.isolatedSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
new CfnRoute(this.parentStack, 'PrivateSubnetPeeringConnectionRoute' + index, {
destinationCidrBlock: '<CIDR>',
routeTableId,
vpcPeeringConnectionId: cfnVPCPeeringConnection.ref,
})
});
You can use a CustomResource Construct in AWS CDK to achieve it:
import * as cdk from "#aws-cdk/core";
import ec2 = require("#aws-cdk/aws-ec2");
import iam = require("#aws-cdk/aws-iam");
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from "#aws-cdk/custom-resources";
import { RetentionDays } from "#aws-cdk/aws-logs";
export interface AllowVPCPeeringDNSResolutionProps {
vpcPeering: ec2.CfnVPCPeeringConnection,
}
export class AllowVPCPeeringDNSResolution extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: AllowVPCPeeringDNSResolutionProps) {
super(scope, id);
const onCreate:AwsSdkCall = {
service: "EC2",
action: "modifyVpcPeeringConnectionOptions",
parameters: {
VpcPeeringConnectionId: props.vpcPeering.ref,
AccepterPeeringConnectionOptions: {
AllowDnsResolutionFromRemoteVpc: true,
},
RequesterPeeringConnectionOptions: {
AllowDnsResolutionFromRemoteVpc: true
}
},
physicalResourceId: PhysicalResourceId.of(`allowVPCPeeringDNSResolution:${props.vpcPeering.ref}`)
};
const onUpdate = onCreate;
const onDelete:AwsSdkCall = {
service: "EC2",
action: "modifyVpcPeeringConnectionOptions",
parameters: {
VpcPeeringConnectionId: props.vpcPeering.ref,
AccepterPeeringConnectionOptions: {
AllowDnsResolutionFromRemoteVpc: false,
},
RequesterPeeringConnectionOptions: {
AllowDnsResolutionFromRemoteVpc: false
}
},
};
const customResource = new AwsCustomResource(this, "allow-peering-dns-resolution", {
policy: AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ["*"],
actions: [
"ec2:ModifyVpcPeeringConnectionOptions",
]
}),
]),
logRetention: RetentionDays.ONE_DAY,
onCreate,
onUpdate,
onDelete,
});
customResource.node.addDependency(props.vpcPeering);
}
}
and use it like this:
[...]
const peerConnection = new ec2.CfnVPCPeeringConnection(this, "peerConnection", {
vpcId: destinationVPC.vpcId,
peerVpcId: lambdaVPCToDestinationVPC.vpcId,
});
new AllowVPCPeeringDNSResolution(this, "peerConnectionDNSResolution", {
vpcPeering: peerConnection,
});
[...]

Update asset files using Tokens with aws-cdk

I have created this stack:
export class InfrastructureStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, "My Hello Website", {
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY
});
const api = new apigateway.RestApi(this, "My Endpoint", {
restApiName: "My rest API name",
description: "Some cool description"
});
const myLambda = new lambda.Function(this, 'My Backend', {
runtime: lambda.Runtime.NODEJS_8_10,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'code'))
});
const apiToLambda = new apigateway.LambdaIntegration(myLambda)
api.root.addMethod('GET', apiToLambda);
updateWebsiteUrl.newUrl(api.url);
}
}
Last line of code is my function to update asset that will be deployed on S3 as a website with a API url that will be created during deployment. This is just a plain Node.js script that replaces files PLACEHOLDER with api.url.
Of course during compile time the CDK does not know what will be the final adress of REST endpoint because this is happening during deploy time and it updates my url with somethis like:
'https://${Token[TOKEN.26]}.execute-api.${Token[AWS::Region.4]}.${Token[AWS::URLSuffix.1]}/${Token[TOKEN.32]}/;'
Is there any way that I can update this after integrating lambda with API endpooint after deploying those?
I would like to use #aws-cdk/aws-s3-deployment module to deploy code to newly created bucket. All in the same Stack, so one cdk deploy will update everything I need.
To avoid confusion. My updateWebsiteUrl is:
export function newUrl(newUrl: string): void {
const scriptPath = path.join(__dirname, '/../../front/');
const scriptName = 'script.js';
fs.readFile(scriptPath + scriptName, (err, buf) => {
let scriptContent : string = buf.toString();
let newScript = scriptContent.replace('URL_PLACEHOLDER', newUrl);
fs.writeFile(scriptPath + 'newScript.js', newScript, () => {
console.log('done writing');
});
});
}
And my script is just simple:
const url = URL_PLACEHOLDER;
function foo() {
let req = new XMLHttpRequest();
req.open('GET', url , false);
req.send(null);
if (req.status == 200) {
replaceContent(req.response);
}
}
function replaceContent(content) {
document.getElementById('content').innerHTML = content;
}
I ran into the same issue today and managed to find a solution for it.
The C# code I am using in my CDK program is the following:
// This will at runtime be just a token which refers to the actual JSON in the format {'api':{'baseUrl':'https://your-url'}}
var configJson = stack.ToJsonString(new Dictionary<string, object>
{
["api"] = new Dictionary<string, object>
{
["baseUrl"] = api.Url
}
});
var configFile = new AwsCustomResource(this, "config-file", new AwsCustomResourceProps
{
OnUpdate = new AwsSdkCall
{
Service = "S3",
Action = "putObject",
Parameters = new Dictionary<string, string>
{
["Bucket"] = bucket.BucketName,
["Key"] = "config.json",
["Body"] = configJson,
["ContentType"] = "application /json",
["CacheControl"] = "max -age=0, no-cache, no-store, must-revalidate"
},
PhysicalResourceId = PhysicalResourceId.Of("config"),
},
Policy = AwsCustomResourcePolicy.FromStatements(
new[]
{
new PolicyStatement(new PolicyStatementProps
{
Actions = new[] { "s3:PutObject" },
Resources= new [] { bucket.ArnForObjects("config.json") }
})
})
});
}
You will need to install the following package to have the types available: https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html
It is basically a part of the solution you can find as an answer to this question AWS CDK passing API Gateway URL to static site in same Stack, or at this GitHub repository: https://github.com/jogold/cloudstructs/blob/master/src/static-website/index.ts#L134

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.
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
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);
}
}
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);
}
}
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.
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.
Get a grab of existing bucket using fromBucketAttributes
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)]
}
);