Skip to content

Commit 1af73c2

Browse files
Update: [AEA-5931] - Add S3 Notifications Lambda (#352)
## Summary Add notification to Slack when files update in S3 ### Details 1. Add new Simple Queue Service (SQS) - SQS triggers when the S3 bucket updates - 60 second buffer for incoming messages 2. SQS triggers lambdas - syncKnowledgeBaseFunction - preprocessingFunction - notifyS3UploadFunction (new) 3. New lambda (notifyS3UploadFunction) - Collects any files changed over 60 seconds - Sends a message to all *private* channels it is installed in 4. Moved syncKnowledgeBaseFunction to handle SQS events - S3 events are provided as a "message" inside SQS events --------- Co-authored-by: Bence Gadanyi <bence.gadanyi1@nhs.net>
1 parent c379a1f commit 1af73c2

26 files changed

Lines changed: 1587 additions & 732 deletions

File tree

.github/workflows/cdk_package_code.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
run: |
6969
poetry show --only=slackBotFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > requirements_slackBotFunction
7070
poetry show --only=syncKnowledgeBaseFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > requirements_syncKnowledgeBaseFunction
71+
poetry show --only=notifyS3UploadFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > requirements_notifyS3UploadFunction
7172
poetry show --only=preprocessingFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > requirements_preprocessingFunction
7273
poetry show --only=bedrockLoggingConfigFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > requirements_bedrockLoggingConfigFunction
7374
if [ ! -s requirements_slackBotFunction ] || [ "$(grep -c -v '^[[:space:]]*$' requirements_slackBotFunction)" -eq 0 ]; then \
@@ -78,6 +79,10 @@ jobs:
7879
echo "Error: requirements_syncKnowledgeBaseFunction is empty or contains only blank lines"; \
7980
exit 1; \
8081
fi
82+
if [ ! -s requirements_notifyS3UploadFunction ] || [ "$(grep -c -v '^[[:space:]]*$' requirements_notifyS3UploadFunction)" -eq 0 ]; then \
83+
echo "Error: requirements_notifyS3UploadFunction is empty or contains only blank lines"; \
84+
exit 1; \
85+
fi
8186
if [ ! -s requirements_preprocessingFunction ] || [ "$(grep -c -v '^[[:space:]]*$' requirements_preprocessingFunction)" -eq 0 ]; then \
8287
echo "Error: requirements_preprocessingFunction is empty or contains only blank lines"; \
8388
exit 1; \
@@ -88,10 +93,14 @@ jobs:
8893
fi
8994
mkdir -p .dependencies/slackBotFunction/python
9095
mkdir -p .dependencies/syncKnowledgeBaseFunction/python
96+
mkdir -p .dependencies/notifyS3UploadFunction/python
9197
mkdir -p .dependencies/preprocessingFunction/python
98+
mkdir -p .dependencies/bedrockLoggingConfigFunction/python
9299
pip3 install -r requirements_slackBotFunction -t .dependencies/slackBotFunction/python
93100
pip3 install -r requirements_syncKnowledgeBaseFunction -t .dependencies/syncKnowledgeBaseFunction/python
94101
pip3 install -r requirements_preprocessingFunction -t .dependencies/preprocessingFunction/python
102+
pip3 install -r requirements_notifyS3UploadFunction -t .dependencies/notifyS3UploadFunction/python
103+
pip3 install -r requirements_bedrockLoggingConfigFunction -t .dependencies/bedrockLoggingConfigFunction/python
95104
rm -rf .dependencies/preprocessingFunction/python/magika* .dependencies/preprocessingFunction/python/onnxruntime*
96105
cp packages/preprocessingFunction/magika_shim.py .dependencies/preprocessingFunction/python/magika.py
97106
find .dependencies/preprocessingFunction/python -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true
@@ -101,10 +110,6 @@ jobs:
101110
find .dependencies/preprocessingFunction/python -type f \( -name "*.pyc" -o -name "*.pyo" -o -name "*.so.debug" \) -delete
102111
find .dependencies/preprocessingFunction/python -type f -name "*.md" ! -name "README.md" -delete
103112
find .dependencies/preprocessingFunction/python -name "*.txt" -size +10k -delete
104-
mkdir -p .dependencies/bedrockLoggingConfigFunction/python
105-
pip3 install -r requirements_slackBotFunction -t .dependencies/slackBotFunction/python
106-
pip3 install -r requirements_syncKnowledgeBaseFunction -t .dependencies/syncKnowledgeBaseFunction/python
107-
pip3 install -r requirements_bedrockLoggingConfigFunction -t .dependencies/bedrockLoggingConfigFunction/python
108113
109114
- name: "Tar files"
110115
run: |

.vscode/eps-assist-me.code-workspace

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
{
1616
"name": "packages/syncKnowledgeBaseFunction",
1717
"path": "../packages/syncKnowledgeBaseFunction"
18+
},
19+
{
20+
"name": "packages/notifyS3UploadFunction",
21+
"path": "../packages/notifyS3UploadFunction"
22+
},
23+
{
24+
"name": "packages/preprocessingFunction",
25+
"path": "../packages/preprocessingFunction"
1826
}
1927
],
2028
"settings": {

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ lint-flake8:
4848
test:
4949
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
5050
cd packages/syncKnowledgeBaseFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
51+
cd packages/notifyS3UploadFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
5152
cd packages/preprocessingFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
5253
cd packages/bedrockLoggingConfigFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
5354

@@ -109,6 +110,7 @@ cdk-synth: cdk-synth-pr cdk-synth-non-pr
109110
cdk-synth-non-pr:
110111
mkdir -p .dependencies/slackBotFunction
111112
mkdir -p .dependencies/syncKnowledgeBaseFunction
113+
mkdir -p .dependencies/notifyS3UploadFunction
112114
mkdir -p .dependencies/preprocessingFunction
113115
mkdir -p .dependencies/bedrockLoggingConfigFunction
114116
mkdir -p .local_config
@@ -129,6 +131,7 @@ cdk-synth-non-pr:
129131
cdk-synth-pr:
130132
mkdir -p .dependencies/slackBotFunction
131133
mkdir -p .dependencies/syncKnowledgeBaseFunction
134+
mkdir -p .dependencies/notifyS3UploadFunction
132135
mkdir -p .dependencies/preprocessingFunction
133136
mkdir -p .dependencies/bedrockLoggingConfigFunction
134137
mkdir -p .local_config

packages/cdk/constructs/S3LambdaNotification.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {Construct} from "constructs"
2+
import {Duration, RemovalPolicy} from "aws-cdk-lib"
3+
import {Queue, QueueEncryption} from "aws-cdk-lib/aws-sqs"
4+
import {Key} from "aws-cdk-lib/aws-kms"
5+
import {SqsEventSource} from "aws-cdk-lib/aws-lambda-event-sources"
6+
import {LambdaFunction} from "./LambdaFunction"
7+
8+
export interface SimpleQueueServiceProps {
9+
readonly stackName: string
10+
readonly queueName: string
11+
readonly batchDelay: number
12+
readonly functions: Array<LambdaFunction>
13+
}
14+
15+
export class SimpleQueueService extends Construct {
16+
public queue: Queue
17+
public deadLetterQueue: Queue
18+
public kmsKey: Key
19+
20+
constructor(scope: Construct, id: string, props: SimpleQueueServiceProps) {
21+
super(scope, id)
22+
23+
const name = `${props.stackName}-${props.queueName}`.toLocaleLowerCase()
24+
25+
const kmsKey = new Key(this, `${name}-queue-key`, {
26+
enableKeyRotation: true,
27+
description: `KMS key for ${props.queueName} queue and dead-letter queue encryption`,
28+
removalPolicy: RemovalPolicy.DESTROY
29+
})
30+
31+
// Create a Dead-Letter Queue (DLQ) for handling failed messages, to help with debugging
32+
const deadLetterQueue = new Queue(this, `${name}-dlq`, {
33+
queueName: `${name}-dlq`,
34+
retentionPeriod: Duration.days(14), // Max 14
35+
encryption: QueueEncryption.KMS,
36+
encryptionMasterKey: kmsKey,
37+
visibilityTimeout: Duration.seconds(60),
38+
enforceSSL: true
39+
})
40+
41+
// Create the main SQS Queue with DLQ configured
42+
const queue = new Queue(this, name,
43+
{
44+
queueName: name,
45+
encryption: QueueEncryption.KMS,
46+
encryptionMasterKey: kmsKey,
47+
deadLetterQueue: {
48+
queue: deadLetterQueue,
49+
maxReceiveCount: 3 // Move to DLQ after 3 failed attempts
50+
},
51+
deliveryDelay: Duration.seconds(0),
52+
visibilityTimeout: Duration.seconds(60),
53+
enforceSSL: true
54+
}
55+
)
56+
57+
// Add queues as event source for the notify function and sync knowledge base function
58+
// While batching, the messages will be sent if maxBatchingWindow is reached or batchSize is reached
59+
// Set (very) large batch size to improve wait efficiency of batching window
60+
const eventSource = new SqsEventSource(queue, {
61+
maxBatchingWindow: Duration.seconds(props.batchDelay),
62+
batchSize: 1000,
63+
reportBatchItemFailures: true
64+
})
65+
66+
props.functions.forEach(fn => {
67+
fn.function.addEventSource(eventSource)
68+
queue.grantConsumeMessages(fn.function)
69+
})
70+
71+
// Grant the Lambda function permissions to consume messages from the queue
72+
73+
this.kmsKey = kmsKey
74+
this.queue = queue
75+
this.deadLetterQueue = deadLetterQueue
76+
}
77+
}

packages/cdk/nagSuppressions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ export const nagSuppressions = (stack: Stack, account: string) => {
2828
]
2929
)
3030

31+
// Suppress wildcard log permissions for NotifyS3UploadFunction Lambda
32+
safeAddNagSuppression(
33+
stack,
34+
"/EpsAssistMeStack/Functions/NotifyS3UploadFunction/LambdaPutLogsManagedPolicy/Resource",
35+
[
36+
{
37+
id: "AwsSolutions-IAM5",
38+
reason: "Wildcard permissions are required for log stream access under known paths."
39+
}
40+
]
41+
)
42+
3143
// Suppress wildcard log permissions for Preprocessing Lambda
3244
safeAddNagSuppression(
3345
stack,

packages/cdk/resources/Functions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ export interface FunctionsProps {
3636
readonly mainSlackBotLambdaExecutionRoleArn : string
3737
readonly ragModelId: string
3838
readonly queryReformulationModelId: string
39+
readonly notifyS3UploadFunctionPolicy: ManagedPolicy
3940
readonly docsBucketName: string
4041
}
4142

4243
export class Functions extends Construct {
4344
public readonly slackBotLambda: LambdaFunction
4445
public readonly syncKnowledgeBaseFunction: LambdaFunction
46+
public readonly notifyS3UploadFunction: LambdaFunction
4547
public readonly preprocessingFunction: LambdaFunction
4648

4749
constructor(scope: Construct, id: string, props: FunctionsProps) {
@@ -133,8 +135,24 @@ export class Functions extends Construct {
133135
additionalPolicies: [props.syncKnowledgeBaseManagedPolicy]
134136
})
135137

138+
const notifyS3UploadFunction = new LambdaFunction(this, "NotifyS3UploadFunction", {
139+
stackName: props.stackName,
140+
functionName: `${props.stackName}-S3UpdateFunction`,
141+
packageBasePath: "packages/notifyS3UploadFunction",
142+
handler: "app.handler.handler",
143+
logRetentionInDays: props.logRetentionInDays,
144+
logLevel: props.logLevel,
145+
dependencyLocation: ".dependencies/notifyS3UploadFunction",
146+
environmentVariables: {
147+
"SLACK_BOT_TOKEN_PARAMETER": props.slackBotTokenParameter.parameterName,
148+
"SLACK_BOT_ACTIVE_ON_PRS": "false"
149+
},
150+
additionalPolicies: [props.notifyS3UploadFunctionPolicy]
151+
})
152+
136153
this.slackBotLambda = slackBotLambda
137154
this.preprocessingFunction = preprocessingFunction
138155
this.syncKnowledgeBaseFunction = syncKnowledgeBaseFunction
156+
this.notifyS3UploadFunction = notifyS3UploadFunction
139157
}
140158
}

packages/cdk/resources/RuntimePolicies.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface RuntimePoliciesProps {
2121
export class RuntimePolicies extends Construct {
2222
public readonly slackBotPolicy: ManagedPolicy
2323
public readonly syncKnowledgeBasePolicy: ManagedPolicy
24+
public readonly notifyS3UploadFunctionPolicy: ManagedPolicy
2425
public readonly preprocessingPolicy: ManagedPolicy
2526

2627
constructor(scope: Construct, id: string, props: RuntimePoliciesProps) {
@@ -51,11 +52,15 @@ export class RuntimePolicies extends Construct {
5152
resources: [props.knowledgeBaseArn]
5253
})
5354

55+
const slackBotPolicyResources = [
56+
`arn:aws:ssm:${props.region}:${props.account}:parameter${props.slackBotTokenParameterName}`,
57+
`arn:aws:ssm:${props.region}:${props.account}:parameter${props.slackSigningSecretParameterName}`
58+
]
59+
5460
const slackBotSSMPolicy = new PolicyStatement({
5561
actions: ["ssm:GetParameter"],
5662
resources: [
57-
`arn:aws:ssm:${props.region}:${props.account}:parameter${props.slackBotTokenParameterName}`,
58-
`arn:aws:ssm:${props.region}:${props.account}:parameter${props.slackSigningSecretParameterName}`
63+
...slackBotPolicyResources
5964
]
6065
})
6166

@@ -136,6 +141,24 @@ export class RuntimePolicies extends Construct {
136141
statements: [syncKnowledgeBasePolicy]
137142
})
138143

144+
// Create managed policy for S3UpdateNotification Lambda function
145+
const notifyS3UploadFunctionPolicy = new PolicyStatement({
146+
actions: [
147+
"ssm:GetParameter",
148+
"sqs:ReceiveMessage",
149+
"sqs:DeleteMessage"
150+
],
151+
resources: [
152+
props.knowledgeBaseArn,
153+
...slackBotPolicyResources
154+
]
155+
})
156+
157+
this.notifyS3UploadFunctionPolicy = new ManagedPolicy(this, "notifyS3UploadFunctionPolicy", {
158+
description: "Policy for S3UpdateNotification Lambda to access SSM parameters",
159+
statements: [notifyS3UploadFunctionPolicy]
160+
})
161+
139162
//policy for the preprocessing lambda
140163
const preprocessingS3Policy = new PolicyStatement({
141164
actions: [

0 commit comments

Comments
 (0)