Skip to content

Commit 947be6a

Browse files
committed
Create reusable construct for S3 Lambda notifications using custom resources
1 parent a3b2bed commit 947be6a

File tree

3 files changed

+102
-40
lines changed

3 files changed

+102
-40
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {Construct} from "constructs"
2+
import {Duration} from "aws-cdk-lib"
3+
import {PolicyStatement} from "aws-cdk-lib/aws-iam"
4+
import {Bucket} from "aws-cdk-lib/aws-s3"
5+
import {Function as LambdaFunction} from "aws-cdk-lib/aws-lambda"
6+
import {AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId} from "aws-cdk-lib/custom-resources"
7+
8+
export interface S3LambdaNotificationProps {
9+
bucket: Bucket
10+
lambdaFunction: LambdaFunction
11+
events?: Array<string>
12+
}
13+
14+
export class S3LambdaNotification extends Construct {
15+
constructor(scope: Construct, id: string, props: S3LambdaNotificationProps) {
16+
super(scope, id)
17+
18+
const events = props.events ?? ["s3:ObjectCreated:*"]
19+
20+
// Create S3 bucket notification using custom resource
21+
new AwsCustomResource(this, "BucketNotification", {
22+
onCreate: {
23+
service: "S3",
24+
action: "putBucketNotificationConfiguration",
25+
parameters: {
26+
Bucket: props.bucket.bucketName,
27+
NotificationConfiguration: {
28+
LambdaConfigurations: [{
29+
Id: "LambdaNotification",
30+
LambdaFunctionArn: props.lambdaFunction.functionArn,
31+
Events: events
32+
}]
33+
}
34+
},
35+
physicalResourceId: PhysicalResourceId.of(`${props.bucket.bucketName}-notification`)
36+
},
37+
onDelete: {
38+
service: "S3",
39+
action: "putBucketNotificationConfiguration",
40+
parameters: {
41+
Bucket: props.bucket.bucketName,
42+
NotificationConfiguration: {}
43+
}
44+
},
45+
policy: AwsCustomResourcePolicy.fromStatements([
46+
new PolicyStatement({
47+
actions: ["s3:PutBucketNotification", "s3:GetBucketNotification"],
48+
resources: [props.bucket.bucketArn]
49+
}),
50+
new PolicyStatement({
51+
actions: ["lambda:AddPermission", "lambda:RemovePermission"],
52+
resources: [props.lambdaFunction.functionArn]
53+
})
54+
]),
55+
timeout: Duration.minutes(5)
56+
})
57+
58+
// Add Lambda permission for S3 to invoke the function
59+
new AwsCustomResource(this, "LambdaPermission", {
60+
onCreate: {
61+
service: "Lambda",
62+
action: "addPermission",
63+
parameters: {
64+
FunctionName: props.lambdaFunction.functionName,
65+
StatementId: "S3InvokePermission",
66+
Action: "lambda:InvokeFunction",
67+
Principal: "s3.amazonaws.com",
68+
SourceArn: props.bucket.bucketArn
69+
},
70+
physicalResourceId: PhysicalResourceId.of(`${props.lambdaFunction.functionName}-s3-permission`)
71+
},
72+
onDelete: {
73+
service: "Lambda",
74+
action: "removePermission",
75+
parameters: {
76+
FunctionName: props.lambdaFunction.functionName,
77+
StatementId: "S3InvokePermission"
78+
}
79+
},
80+
policy: AwsCustomResourcePolicy.fromStatements([
81+
new PolicyStatement({
82+
actions: ["lambda:AddPermission", "lambda:RemovePermission"],
83+
resources: [props.lambdaFunction.functionArn]
84+
})
85+
])
86+
})
87+
}
88+
}

packages/cdk/nagSuppressions.ts

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -214,60 +214,35 @@ export const nagSuppressions = (stack: Stack) => {
214214
]
215215
)
216216

217-
// Suppress AWS managed policy usage in BucketNotificationsHandler
217+
// Suppress custom resource IAM permissions for S3 Lambda notification
218218
safeAddNagSuppression(
219219
stack,
220-
"/EpsAssistMeStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Role/Resource",
220+
"/EpsAssistMeStack/S3ToSyncKnowledgeBase/BucketNotification/CustomResourcePolicy/Resource",
221221
[
222222
{
223-
id: "AwsSolutions-IAM4",
224-
reason: "Auto-generated CDK role uses AWS managed policy for basic Lambda execution.",
223+
id: "AwsSolutions-IAM5",
224+
reason: "Custom resource requires wildcard permissions to manage S3 bucket notifications and Lambda permissions.",
225225
appliesTo: [
226-
"Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
226+
"Resource::*"
227227
]
228228
}
229229
]
230230
)
231231

232-
// Suppress wildcard permissions for BucketNotificationsHandler default policy
233232
safeAddNagSuppression(
234233
stack,
235-
"/EpsAssistMeStack/BucketNotificationsHandler050a0587b7544547bf325f094a3db834/Role/DefaultPolicy/Resource",
234+
"/EpsAssistMeStack/S3ToSyncKnowledgeBase/LambdaPermission/CustomResourcePolicy/Resource",
236235
[
237236
{
238237
id: "AwsSolutions-IAM5",
239-
reason: "Auto-generated CDK role requires wildcard permissions for S3 bucket notifications.",
238+
reason: "Custom resource requires wildcard permissions to manage Lambda permissions.",
240239
appliesTo: [
241240
"Resource::*"
242241
]
243242
}
244243
]
245244
)
246245

247-
// Suppress Lambda function public access for S3 service principal
248-
safeAddNagSuppression(
249-
stack,
250-
"/EpsAssistMeStack/S3ToSyncKnowledgeBaseLambdaPermission",
251-
[
252-
{
253-
id: "LAMBDA_FUNCTION_PUBLIC_ACCESS_PROHIBITED",
254-
reason: "S3 service principal access is required for bucket notifications to trigger Lambda function."
255-
}
256-
]
257-
)
258-
259-
// Suppress Lambda function public access for S3 bucket notifications
260-
safeAddNagSuppression(
261-
stack,
262-
`/EpsAssistMeStack/Storage/DocsBucket/${stackName}-Docs/AllowBucketNotificationsToEpsAssistMeStackFunctionsSyncKnowledgeBaseFunctionepsamSyncKnowledgeBaseFunction94D011F3`,
263-
[
264-
{
265-
id: "LAMBDA_FUNCTION_PUBLIC_ACCESS_PROHIBITED",
266-
reason: "S3 service principal access is required for bucket notifications to trigger Lambda function."
267-
}
268-
]
269-
)
270-
271246
// Suppress Lambda function public access for API Gateway permissions
272247
safeAddNagSuppression(
273248
stack,

packages/cdk/stacks/EpsAssistMeStack.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import {
44
StackProps,
55
CfnOutput
66
} from "aws-cdk-lib"
7-
import {EventType} from "aws-cdk-lib/aws-s3"
8-
import {LambdaDestination} from "aws-cdk-lib/aws-s3-notifications"
97
import {nagSuppressions} from "../nagSuppressions"
108
import {Apis} from "../resources/Apis"
119
import {Functions} from "../resources/Functions"
@@ -15,6 +13,7 @@ import {OpenSearchResources} from "../resources/OpenSearchResources"
1513
import {VectorKnowledgeBaseResources} from "../resources/VectorKnowledgeBaseResources"
1614
import {IamResources} from "../resources/IamResources"
1715
import {VectorIndex} from "../resources/VectorIndex"
16+
import {S3LambdaNotification} from "../constructs/S3LambdaNotification"
1817

1918
const VECTOR_INDEX_NAME = "eps-assist-os-index"
2019

@@ -133,12 +132,6 @@ export class EpsAssistMeStack extends Stack {
133132
vectorKB.dataSource.attrDataSourceId
134133
)
135134

136-
// Add S3 event notification to trigger sync function
137-
storage.kbDocsBucket.bucket.addEventNotification(
138-
EventType.OBJECT_CREATED,
139-
new LambdaDestination(functions.functions.syncKnowledgeBase.function)
140-
)
141-
142135
// Create Apis and pass the Lambda function
143136
const apis = new Apis(this, "Apis", {
144137
stackName: props.stackName,
@@ -149,6 +142,12 @@ export class EpsAssistMeStack extends Stack {
149142
}
150143
})
151144

145+
// Setup S3 to Lambda notification
146+
new S3LambdaNotification(this, "S3ToSyncKnowledgeBase", {
147+
bucket: storage.kbDocsBucket.bucket,
148+
lambdaFunction: functions.functions.syncKnowledgeBase.function
149+
})
150+
152151
// Output: SlackBot Endpoint
153152
new CfnOutput(this, "SlackBotEndpoint", {
154153
value: `https://${apis.apis["api"].api.domainName?.domainName}/slack/ask-eps`

0 commit comments

Comments
 (0)