223 lines
6.6 KiB
TypeScript
223 lines
6.6 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 - scoped to this Lambda's log group
|
|
lambdaRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
effect: iam.Effect.ALLOW,
|
|
actions: [
|
|
"logs:CreateLogGroup",
|
|
"logs:CreateLogStream",
|
|
"logs:PutLogEvents",
|
|
],
|
|
resources: [
|
|
`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/image-processor-${environment}`,
|
|
`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/image-processor-${environment}:*`,
|
|
],
|
|
})
|
|
);
|
|
|
|
// 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}`,
|
|
});
|
|
}
|
|
}
|