Skip to content

Commit 75607d6

Browse files
devksingh4eeen17
authored andcommitted
Setup async process handling via SQS (#45)
* build a generalizable setup for async sqs handling * setup sqs queues * fix cfn * fix missing max recieve count * fix build entrypoints * create the appropriate roles * fix cfn Signed-off-by: Dev Singh <dsingh14@illinois.edu> * enable partial batch item failures Signed-off-by: Dev Singh <dsingh14@illinois.edu> * fix consumer Signed-off-by: Dev Singh <dsingh14@illinois.edu> * fix alarm metric Signed-off-by: Dev Singh <dsingh14@illinois.edu> * setup basic driver * move membership pass creation into SQS queue * move sqs unit tests * fix unit test * fix mobilewallet route test * cleanup * fix iam --------- Signed-off-by: Dev Singh <dsingh14@illinois.edu>
1 parent a36436e commit 75607d6

21 files changed

+622
-112
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ build: src/ cloudformation/ docs/
5454
yarn -D
5555
VITE_BUILD_HASH=$(GIT_HASH) yarn build
5656
cp -r src/api/resources/ dist/api/resources
57+
rm -rf dist/lambda/sqs
5758
sam build --template-file cloudformation/main.yml
5859

5960
local:

cloudformation/iam.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ Parameters:
1212
AllowedPattern: ^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$
1313
SesEmailDomain:
1414
Type: String
15+
SqsQueueArn:
16+
Type: String
1517
Resources:
1618
ApiLambdaIAMRole:
1719
Type: AWS::IAM::Role
1820
Properties:
21+
ManagedPolicyArns:
22+
- arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole
1923
AssumeRolePolicyDocument:
2024
Version: '2012-10-17'
2125
Statement:
@@ -41,6 +45,14 @@ Resources:
4145
ses:Recipients:
4246
- "*@illinois.edu"
4347
PolicyName: ses-membership
48+
- PolicyDocument:
49+
Version: '2012-10-17'
50+
Statement:
51+
- Action:
52+
- sqs:SendMessage
53+
Effect: Allow
54+
Resource: !Ref SqsQueueArn
55+
PolicyName: lambda-sqs
4456
- PolicyDocument:
4557
Version: '2012-10-17'
4658
Statement:

cloudformation/main.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ Parameters:
2222
Default: false
2323
Type: String
2424
AllowedValues: [true, false]
25+
SqsLambdaTimeout:
26+
Description: How long the SQS lambda is permitted to run (in seconds)
27+
Default: 300
28+
Type: Number
29+
SqsMessageTimeout:
30+
Description: MessageVisibilityTimeout for the SQS Lambda queue (should be at least 6xSqsLambdaTimeout)
31+
Default: 1800
32+
Type: Number
2533

2634
Conditions:
2735
IsProd: !Equals [!Ref RunEnvironment, 'prod']
@@ -74,6 +82,7 @@ Resources:
7482
RunEnvironment: !Ref RunEnvironment
7583
LambdaFunctionName: !Sub ${ApplicationPrefix}-lambda
7684
SesEmailDomain: !FindInMap [General, !Ref RunEnvironment, SesDomain]
85+
SqsQueueArn: !GetAtt AppSQSQueues.Outputs.MainQueueArn
7786

7887
AppLogGroups:
7988
Type: AWS::Serverless::Application
@@ -83,6 +92,14 @@ Resources:
8392
LambdaFunctionName: !Sub ${ApplicationPrefix}-lambda
8493
LogRetentionDays: !FindInMap [General, !Ref RunEnvironment, LogRetentionDays]
8594

95+
AppSQSQueues:
96+
Type: AWS::Serverless::Application
97+
Properties:
98+
Location: ./sqs.yml
99+
Parameters:
100+
QueueName: !Sub ${ApplicationPrefix}-sqs
101+
MessageTimeout: !Ref SqsMessageTimeout
102+
86103
IcalDomainProxy:
87104
Type: AWS::Serverless::Application
88105
Properties:
@@ -149,6 +166,40 @@ Resources:
149166
Path: /{proxy+}
150167
Method: ANY
151168

169+
AppSqsLambdaFunction:
170+
Type: AWS::Serverless::Function
171+
DependsOn:
172+
- AppLogGroups
173+
Properties:
174+
Architectures: [arm64]
175+
CodeUri: ../dist/sqsConsumer
176+
AutoPublishAlias: live
177+
Runtime: nodejs22.x
178+
Description: !Sub "${ApplicationFriendlyName} SQS Lambda"
179+
FunctionName: !Sub ${ApplicationPrefix}-sqs-lambda
180+
Handler: index.handler
181+
MemorySize: 512
182+
Role: !GetAtt AppSecurityRoles.Outputs.MainFunctionRoleArn
183+
Timeout: !Ref SqsLambdaTimeout
184+
LoggingConfig:
185+
LogGroup: !Sub /aws/lambda/${ApplicationPrefix}-lambda
186+
Environment:
187+
Variables:
188+
RunEnvironment: !Ref RunEnvironment
189+
VpcConfig:
190+
Ipv6AllowedForDualStack: !If [ShouldAttachVpc, True, !Ref AWS::NoValue]
191+
SecurityGroupIds: !If [ShouldAttachVpc, !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SecurityGroupIds], !Ref AWS::NoValue]
192+
SubnetIds: !If [ShouldAttachVpc, !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SubnetIds], !Ref AWS::NoValue]
193+
194+
SQSLambdaEventMapping:
195+
Type: AWS::Lambda::EventSourceMapping
196+
Properties:
197+
BatchSize: 5
198+
EventSourceArn: !GetAtt AppSQSQueues.Outputs.MainQueueArn
199+
FunctionName: !Sub ${ApplicationPrefix}-sqs-lambda
200+
FunctionResponseTypes:
201+
- ReportBatchItemFailures
202+
152203
IamGroupRolesTable:
153204
Type: 'AWS::DynamoDB::Table'
154205
DeletionPolicy: "Retain"
@@ -348,6 +399,23 @@ Resources:
348399
- Name: 'ApiName'
349400
Value: !Sub ${ApplicationPrefix}-gateway
350401

402+
403+
AppDLQMessagesAlarm:
404+
Type: 'AWS::CloudWatch::Alarm'
405+
Condition: IsProd
406+
Properties:
407+
AlarmName: !Sub ${ApplicationPrefix}-sqs-dlq
408+
AlarmDescription: 'Items are present in the application DLQ, meaning some messages failed to process.'
409+
Namespace: 'AWS/SQS'
410+
MetricName: 'ApproximateNumberOfMessagesVisible'
411+
Statistic: 'Sum'
412+
Period: '60'
413+
EvaluationPeriods: '1'
414+
ComparisonOperator: 'GreaterThanThreshold'
415+
Threshold: '0'
416+
AlarmActions:
417+
- !Ref AlertSNSArn
418+
351419
APILambdaPermission:
352420
Type: AWS::Lambda::Permission
353421
Properties:

cloudformation/sqs.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Description: Stack SQS Queues
3+
Transform: AWS::Serverless-2016-10-31
4+
Parameters:
5+
QueueName:
6+
Type: String
7+
AllowedPattern: ^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$
8+
MessageTimeout:
9+
Type: Number
10+
Resources:
11+
AppDLQ:
12+
Type: AWS::SQS::Queue
13+
Properties:
14+
QueueName: !Sub ${QueueName}-dlq
15+
VisibilityTimeout: !Ref MessageTimeout
16+
AppQueue:
17+
Type: AWS::SQS::Queue
18+
Properties:
19+
QueueName: !Ref QueueName
20+
VisibilityTimeout: !Ref MessageTimeout
21+
RedrivePolicy:
22+
deadLetterTargetArn:
23+
Fn::GetAtt:
24+
- "AppDLQ"
25+
- "Arn"
26+
maxReceiveCount: 3
27+
28+
Outputs:
29+
MainQueueArn:
30+
Description: Main Queue Arn
31+
Value:
32+
Fn::GetAtt:
33+
- AppQueue
34+
- Arn
35+
DLQArn:
36+
Description: Dead-letter Queue Arn
37+
Value:
38+
Fn::GetAtt:
39+
- AppDLQ
40+
- Arn

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
"scripts": {
1212
"build": "yarn workspaces run build && yarn lockfile-manage",
1313
"dev": "concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
14-
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp src/api/package.lambda.json dist/lambda/package.json && rm package-lock.json",
14+
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp package-lock.json dist/sqsConsumer/ && cp src/api/package.lambda.json dist/lambda/package.json && cp src/api/package.lambda.json dist/sqsConsumer/package.json && rm package-lock.json",
1515
"prettier": "yarn workspaces run prettier && prettier --check tests/**/*.ts",
1616
"prettier:write": "yarn workspaces run prettier:write && prettier --write tests/**/*.ts",
1717
"lint": "yarn workspaces run lint",
1818
"prepare": "node .husky/install.mjs || true",
1919
"typecheck": "yarn workspaces run typecheck",
20-
"test:unit": "vitest run tests/unit --config tests/unit/vitest.config.ts && yarn workspace infra-core-ui run test:unit",
20+
"test:unit": "cross-env RunEnvironment='dev' vitest run tests/unit --config tests/unit/vitest.config.ts && yarn workspace infra-core-ui run test:unit",
2121
"test:unit-ui": "yarn test:unit --ui",
2222
"test:unit-watch": "vitest tests/unit",
2323
"test:live": "vitest tests/live",
@@ -39,7 +39,7 @@
3939
"@typescript-eslint/parser": "^8.0.1",
4040
"@vitejs/plugin-react": "^4.3.1",
4141
"@vitest/ui": "^2.0.5",
42-
"aws-sdk-client-mock": "^4.0.1",
42+
"aws-sdk-client-mock": "^4.1.0",
4343
"concurrently": "^9.1.2",
4444
"cross-env": "^7.0.3",
4545
"esbuild": "^0.23.0",
@@ -81,4 +81,4 @@
8181
"resolutions": {
8282
"pdfjs-dist": "^4.8.69"
8383
}
84-
}
84+
}

src/api/build.js

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
import esbuild from "esbuild";
22
import { resolve } from "path";
33

4+
5+
const commonParams = {
6+
bundle: true,
7+
format: "esm",
8+
minify: true,
9+
outExtension: { ".js": ".mjs" },
10+
loader: {
11+
".png": "file",
12+
".pkpass": "file",
13+
".json": "file",
14+
}, // File loaders
15+
target: "es2022", // Target ES2022
16+
sourcemap: false,
17+
platform: "node",
18+
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"],
19+
alias: {
20+
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
21+
},
22+
banner: {
23+
js: `
24+
import path from 'path';
25+
import { fileURLToPath } from 'url';
26+
import { createRequire as topLevelCreateRequire } from 'module';
27+
const require = topLevelCreateRequire(import.meta.url);
28+
const __filename = fileURLToPath(import.meta.url);
29+
const __dirname = path.dirname(__filename);
30+
`.trim(),
31+
}, // Banner for compatibility with CommonJS
32+
}
433
esbuild
534
.build({
6-
entryPoints: ["api/lambda.js"], // Entry file
7-
bundle: true,
8-
format: "esm",
9-
minify: true,
35+
...commonParams,
36+
entryPoints: ["api/lambda.js"],
1037
outdir: "../../dist/lambda/",
11-
outExtension: { ".js": ".mjs" },
12-
loader: {
13-
".png": "file",
14-
".pkpass": "file",
15-
".json": "file",
16-
}, // File loaders
17-
target: "es2022", // Target ES2022
18-
sourcemap: false,
19-
platform: "node",
20-
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"],
21-
alias: {
22-
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
23-
},
24-
banner: {
25-
js: `
26-
import path from 'path';
27-
import { fileURLToPath } from 'url';
28-
import { createRequire as topLevelCreateRequire } from 'module';
29-
const require = topLevelCreateRequire(import.meta.url);
30-
const __filename = fileURLToPath(import.meta.url);
31-
const __dirname = path.dirname(__filename);
32-
`.trim(),
33-
}, // Banner for compatibility with CommonJS
38+
external: [...commonParams.external, "sqs/*"],
39+
})
40+
.then(() => console.log("API server build completed successfully!"))
41+
.catch((error) => {
42+
console.error("API server build failed:", error);
43+
process.exit(1);
44+
});
45+
46+
esbuild
47+
.build({
48+
...commonParams,
49+
entryPoints: ["api/sqs/index.js", "api/sqs/driver.js"],
50+
outdir: "../../dist/sqsConsumer/",
3451
})
35-
.then(() => console.log("Build completed successfully!"))
52+
.then(() => console.log("SQS consumer build completed successfully!"))
3653
.catch((error) => {
37-
console.error("Build failed:", error);
54+
console.error("SQS consumer build failed:", error);
3855
process.exit(1);
3956
});

src/api/functions/entraId.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ import {
2121
} from "../../common/types/iam.js";
2222
import { FastifyInstance } from "fastify";
2323
import { UserProfileDataBase } from "common/types/msGraphApi.js";
24+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
25+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2426

2527
function validateGroupId(groupId: string): boolean {
2628
const groupIdPattern = /^[a-zA-Z0-9-]+$/; // Adjust the pattern as needed
2729
return groupIdPattern.test(groupId);
2830
}
2931

3032
export async function getEntraIdToken(
31-
fastify: FastifyInstance,
33+
clients: { smClient: SecretsManagerClient; dynamoClient: DynamoDBClient },
3234
clientId: string,
3335
scopes: string[] = ["https://graph.microsoft.com/.default"],
3436
) {
3537
const secretApiConfig =
36-
(await getSecretValue(
37-
fastify.secretsManagerClient,
38-
genericConfig.ConfigSecretName,
39-
)) || {};
38+
(await getSecretValue(clients.smClient, genericConfig.ConfigSecretName)) ||
39+
{};
4040
if (
4141
!secretApiConfig.entra_id_private_key ||
4242
!secretApiConfig.entra_id_thumbprint
@@ -50,7 +50,7 @@ export async function getEntraIdToken(
5050
"base64",
5151
).toString("utf8");
5252
const cachedToken = await getItemFromCache(
53-
fastify.dynamoClient,
53+
clients.dynamoClient,
5454
"entra_id_access_token",
5555
);
5656
if (cachedToken) {
@@ -80,7 +80,7 @@ export async function getEntraIdToken(
8080
date.setTime(date.getTime() - 30000);
8181
if (result?.accessToken) {
8282
await insertItemIntoCache(
83-
fastify.dynamoClient,
83+
clients.dynamoClient,
8484
"entra_id_access_token",
8585
{ token: result?.accessToken },
8686
date,

0 commit comments

Comments
 (0)