image processing lambda

This commit is contained in:
jackiettran
2026-01-14 12:11:50 -05:00
parent f5fdcbfb82
commit da82872297
15 changed files with 8090 additions and 17 deletions

View File

@@ -2,6 +2,7 @@
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { ConditionCheckLambdaStack } from "../lib/condition-check-lambda-stack";
import { ImageProcessorLambdaStack } from "../lib/image-processor-lambda-stack";
const app = new cdk.App();
@@ -43,7 +44,7 @@ if (!config) {
throw new Error(`Unknown environment: ${environment}`);
}
// Create the Lambda stack
// Create the Condition Check Lambda stack
new ConditionCheckLambdaStack(app, `ConditionCheckLambdaStack-${environment}`, {
environment,
databaseUrl: config.databaseUrl,
@@ -57,7 +58,24 @@ new ConditionCheckLambdaStack(app, `ConditionCheckLambdaStack-${environment}`, {
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
tags: {
Environment: environment,
Project: "rentall",
Project: "village-share",
Service: "condition-check-reminder",
},
});
// Create the Image Processor Lambda stack
new ImageProcessorLambdaStack(app, `ImageProcessorLambdaStack-${environment}`, {
environment,
databaseUrl: config.databaseUrl,
frontendUrl: config.frontendUrl,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
},
description: `Image Processor Lambda infrastructure (${environment})`,
tags: {
Environment: environment,
Project: "village-share",
Service: "image-processor",
},
});

View File

@@ -0,0 +1,219 @@
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3n from "aws-cdk-lib/aws-s3-notifications";
import * as sqs from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";
import * as path from "path";
interface ImageProcessorLambdaStackProps extends cdk.StackProps {
/**
* Environment name (staging, prod)
*/
environment: string;
/**
* Database URL for the Lambda
*/
databaseUrl: string;
/**
* Frontend URL for CORS configuration
*/
frontendUrl: string;
}
export class ImageProcessorLambdaStack extends cdk.Stack {
/**
* The Lambda function for image processing
*/
public readonly lambdaFunction: lambda.Function;
/**
* The S3 bucket for image uploads
*/
public readonly uploadsBucket: s3.Bucket;
/**
* Dead letter queue for failed Lambda invocations
*/
public readonly deadLetterQueue: sqs.Queue;
constructor(
scope: Construct,
id: string,
props: ImageProcessorLambdaStackProps
) {
super(scope, id, props);
const { environment, databaseUrl, frontendUrl } = props;
// Dead Letter Queue for failed Lambda invocations
this.deadLetterQueue = new sqs.Queue(this, "ImageProcessorDLQ", {
queueName: `image-processor-dlq-${environment}`,
retentionPeriod: cdk.Duration.days(14),
});
// S3 bucket for uploads
this.uploadsBucket = new s3.Bucket(this, "UploadsBucket", {
bucketName: `village-share-${environment}`,
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: true,
blockPublicPolicy: false, // Allow bucket policy for public reads
ignorePublicAcls: true,
restrictPublicBuckets: false,
}),
cors: [
{
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.PUT,
s3.HttpMethods.POST,
],
allowedOrigins: [frontendUrl, "http://localhost:3000"],
allowedHeaders: ["*"],
exposedHeaders: ["ETag"],
maxAge: 3600,
},
],
lifecycleRules: [
{
// Clean up incomplete multipart uploads
abortIncompleteMultipartUploadAfter: cdk.Duration.days(1),
},
{
// Delete staging files that weren't processed after 7 days
prefix: "staging/",
expiration: cdk.Duration.days(7),
},
],
});
// Bucket policy: allow public read for non-staging files
this.uploadsBucket.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.AnyPrincipal()],
actions: ["s3:GetObject"],
resources: [
`${this.uploadsBucket.bucketArn}/profiles/*`,
`${this.uploadsBucket.bucketArn}/items/*`,
`${this.uploadsBucket.bucketArn}/forum/*`,
],
})
);
// Lambda execution role
const lambdaRole = new iam.Role(this, "ImageProcessorLambdaRole", {
roleName: `image-processor-lambda-role-${environment}`,
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
description: "Execution role for Image Processor Lambda",
});
// CloudWatch Logs permissions
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: ["*"],
})
);
// S3 permissions
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:HeadObject",
],
resources: [`${this.uploadsBucket.bucketArn}/*`],
})
);
// Lambda function
this.lambdaFunction = new lambda.Function(this, "ImageProcessorLambda", {
functionName: `image-processor-${environment}`,
runtime: lambda.Runtime.NODEJS_20_X,
handler: "index.handler",
code: lambda.Code.fromAsset(
path.join(__dirname, "../../../lambdas/imageProcessor"),
{
bundling: {
image: lambda.Runtime.NODEJS_20_X.bundlingImage,
command: [
"bash",
"-c",
[
"cp -r /asset-input/* /asset-output/",
"cd /asset-output",
"npm install --omit=dev",
// Copy shared modules
"mkdir -p shared",
"cp -r /asset-input/../shared/* shared/",
"cd shared && npm install --omit=dev",
].join(" && "),
],
},
}
),
role: lambdaRole,
timeout: cdk.Duration.seconds(60),
memorySize: 1024, // Higher memory for image processing
environment: {
NODE_ENV: environment,
DATABASE_URL: databaseUrl,
S3_BUCKET: this.uploadsBucket.bucketName,
AWS_REGION: this.region,
LOG_LEVEL: environment === "prod" ? "info" : "debug",
},
deadLetterQueue: this.deadLetterQueue,
retryAttempts: 2,
description:
"Processes uploaded images: extracts metadata and strips EXIF",
});
// S3 event notification for staging uploads
this.uploadsBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(this.lambdaFunction),
{
prefix: "staging/",
}
);
// Outputs
new cdk.CfnOutput(this, "LambdaFunctionArn", {
value: this.lambdaFunction.functionArn,
description: "ARN of the Image Processor Lambda",
exportName: `ImageProcessorLambdaArn-${environment}`,
});
new cdk.CfnOutput(this, "UploadsBucketName", {
value: this.uploadsBucket.bucketName,
description: "Name of the uploads S3 bucket",
exportName: `UploadsBucketName-${environment}`,
});
new cdk.CfnOutput(this, "UploadsBucketArn", {
value: this.uploadsBucket.bucketArn,
description: "ARN of the uploads S3 bucket",
exportName: `UploadsBucketArn-${environment}`,
});
new cdk.CfnOutput(this, "DLQUrl", {
value: this.deadLetterQueue.queueUrl,
description: "URL of the Dead Letter Queue",
exportName: `ImageProcessorDLQUrl-${environment}`,
});
}
}

487
infrastructure/cdk/package-lock.json generated Normal file
View File

@@ -0,0 +1,487 @@
{
"name": "rentall-infrastructure",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rentall-infrastructure",
"version": "1.0.0",
"dependencies": {
"aws-cdk-lib": "^2.170.0",
"constructs": "^10.4.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"aws-cdk": "^2.170.0",
"typescript": "^5.7.0"
}
},
"node_modules/@aws-cdk/asset-awscli-v1": {
"version": "2.2.258",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.258.tgz",
"integrity": "sha512-TL3I9cIue0bAsuwrmjgjAQaEH6JL09y49FVQMDhrz4jJ2iPKuHtdrYd7ydm02t1YZdPZE2M0VNj6VD4fGIFpvw==",
"license": "Apache-2.0"
},
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz",
"integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==",
"license": "Apache-2.0"
},
"node_modules/@aws-cdk/cloud-assembly-schema": {
"version": "48.20.0",
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.20.0.tgz",
"integrity": "sha512-+eeiav9LY4wbF/EFuCt/vfvi/Zoxo8bf94PW5clbMraChEliq83w4TbRVy0jB9jE0v1ooFTtIjSQkowSPkfISg==",
"bundleDependencies": [
"jsonschema",
"semver"
],
"license": "Apache-2.0",
"dependencies": {
"jsonschema": "~1.4.1",
"semver": "^7.7.2"
},
"engines": {
"node": ">= 18.0.0"
}
},
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
"version": "1.4.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
"version": "7.7.2",
"inBundle": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/node": {
"version": "22.19.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
"integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/aws-cdk": {
"version": "2.1100.3",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.3.tgz",
"integrity": "sha512-jeSamF+IwPJKhqMir7Cw+2IoeHsmNFc/SoDAlOS9BYM8Wrd0Q1jJd3GcJOFzsMcWv9mcBAP5o23amyKHu03dXA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"cdk": "bin/cdk"
},
"engines": {
"node": ">= 18.0.0"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/aws-cdk-lib": {
"version": "2.234.1",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.234.1.tgz",
"integrity": "sha512-2oNqAA1qjF9xHCom6yHuY8KE6UltK7pTg3egf/t1+C6/OFEaw9+jyhCWmTasGmvjyQSkbvKiCPZco0l+XVyxiQ==",
"bundleDependencies": [
"@balena/dockerignore",
"case",
"fs-extra",
"ignore",
"jsonschema",
"minimatch",
"punycode",
"semver",
"table",
"yaml",
"mime-types"
],
"license": "Apache-2.0",
"dependencies": {
"@aws-cdk/asset-awscli-v1": "2.2.258",
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0",
"@aws-cdk/cloud-assembly-schema": "^48.20.0",
"@balena/dockerignore": "^1.0.2",
"case": "1.6.3",
"fs-extra": "^11.3.3",
"ignore": "^5.3.2",
"jsonschema": "^1.5.0",
"mime-types": "^2.1.35",
"minimatch": "^3.1.2",
"punycode": "^2.3.1",
"semver": "^7.7.3",
"table": "^6.9.0",
"yaml": "1.10.2"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"constructs": "^10.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
"version": "1.0.2",
"inBundle": true,
"license": "Apache-2.0"
},
"node_modules/aws-cdk-lib/node_modules/ajv": {
"version": "8.17.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/aws-cdk-lib/node_modules/ansi-regex": {
"version": "5.0.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/ansi-styles": {
"version": "4.3.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aws-cdk-lib/node_modules/astral-regex": {
"version": "2.0.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
"version": "1.0.2",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
"version": "1.1.12",
"inBundle": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/aws-cdk-lib/node_modules/case": {
"version": "1.6.3",
"inBundle": true,
"license": "(MIT OR GPL-3.0-or-later)",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/aws-cdk-lib/node_modules/color-convert": {
"version": "2.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/color-name": {
"version": "1.1.4",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/concat-map": {
"version": "0.0.1",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/emoji-regex": {
"version": "8.0.0",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/fast-deep-equal": {
"version": "3.1.3",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/fast-uri": {
"version": "3.1.0",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"inBundle": true,
"license": "BSD-3-Clause"
},
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
"version": "11.3.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
"version": "4.2.11",
"inBundle": true,
"license": "ISC"
},
"node_modules/aws-cdk-lib/node_modules/ignore": {
"version": "5.3.2",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/json-schema-traverse": {
"version": "1.0.0",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
"version": "6.2.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
"version": "1.5.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/aws-cdk-lib/node_modules/lodash.truncate": {
"version": "4.4.2",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/mime-db": {
"version": "1.52.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/aws-cdk-lib/node_modules/mime-types": {
"version": "2.1.35",
"inBundle": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/aws-cdk-lib/node_modules/minimatch": {
"version": "3.1.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/aws-cdk-lib/node_modules/punycode": {
"version": "2.3.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/aws-cdk-lib/node_modules/require-from-string": {
"version": "2.0.2",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/aws-cdk-lib/node_modules/semver": {
"version": "7.7.3",
"inBundle": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aws-cdk-lib/node_modules/slice-ansi": {
"version": "4.0.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/aws-cdk-lib/node_modules/string-width": {
"version": "4.2.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/strip-ansi": {
"version": "6.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/table": {
"version": "6.9.0",
"inBundle": true,
"license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/universalify": {
"version": "2.0.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/yaml": {
"version": "1.10.2",
"inBundle": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/constructs": {
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.4.tgz",
"integrity": "sha512-lP0qC1oViYf1cutHo9/KQ8QL637f/W29tDmv/6sy35F5zs+MD9f66nbAAIjicwc7fwyuF3rkg6PhZh4sfvWIpA==",
"license": "Apache-2.0"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}