Files
rentall-app/infrastructure/cdk/lib/image-processor-lambda-stack.ts
2026-01-14 12:11:50 -05:00

220 lines
6.4 KiB
TypeScript

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}`,
});
}
}