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 scheduler from "aws-cdk-lib/aws-scheduler"; import * as sqs from "aws-cdk-lib/aws-sqs"; import { Construct } from "constructs"; import * as path from "path"; interface ConditionCheckLambdaStackProps extends cdk.StackProps { /** * Environment name (staging, prod) */ environment: string; /** * Database URL for the Lambda */ databaseUrl: string; /** * Frontend URL for email links */ frontendUrl: string; /** * SES sender email */ sesFromEmail: string; /** * SES sender name */ sesFromName?: string; /** * Whether emails are enabled */ emailEnabled?: boolean; } export class ConditionCheckLambdaStack extends cdk.Stack { /** * The Lambda function for condition check reminders */ public readonly lambdaFunction: lambda.Function; /** * The EventBridge Scheduler group for condition check schedules */ public readonly scheduleGroup: scheduler.CfnScheduleGroup; /** * Dead letter queue for failed Lambda invocations */ public readonly deadLetterQueue: sqs.Queue; /** * IAM role for EventBridge Scheduler to invoke Lambda */ public readonly schedulerRole: iam.Role; constructor( scope: Construct, id: string, props: ConditionCheckLambdaStackProps ) { super(scope, id, props); const { environment, databaseUrl, frontendUrl, sesFromEmail, sesFromName = "Village Share", emailEnabled = true, } = props; // Dead Letter Queue for failed Lambda invocations this.deadLetterQueue = new sqs.Queue(this, "ConditionCheckDLQ", { queueName: `condition-check-reminder-dlq-${environment}`, retentionPeriod: cdk.Duration.days(14), }); // Lambda execution role const lambdaRole = new iam.Role(this, "ConditionCheckLambdaRole", { roleName: `condition-check-lambda-role-${environment}`, assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), description: "Execution role for Condition Check Reminder 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/condition-check-reminder-${environment}`, `arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/condition-check-reminder-${environment}:*`, ], }) ); // SES permissions for sending emails - scoped to verified identity lambdaRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["ses:SendEmail", "ses:SendRawEmail"], resources: [ `arn:aws:ses:${this.region}:${this.account}:identity/${sesFromEmail}`, ], }) ); // EventBridge Scheduler permissions (for self-cleanup) lambdaRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["scheduler:DeleteSchedule"], resources: [ `arn:aws:scheduler:${this.region}:${this.account}:schedule/condition-check-reminders-${environment}/*`, ], }) ); // Lambda function this.lambdaFunction = new lambda.Function( this, "ConditionCheckReminderLambda", { functionName: `condition-check-reminder-${environment}`, runtime: lambda.Runtime.NODEJS_20_X, handler: "index.handler", code: lambda.Code.fromAsset( path.join(__dirname, "../../../lambdas/conditionCheckReminder"), { 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(30), memorySize: 256, environment: { NODE_ENV: environment, DATABASE_URL: databaseUrl, FRONTEND_URL: frontendUrl, SES_FROM_EMAIL: sesFromEmail, SES_FROM_NAME: sesFromName, EMAIL_ENABLED: emailEnabled ? "true" : "false", SCHEDULE_GROUP_NAME: `condition-check-reminders-${environment}`, AWS_REGION: this.region, }, deadLetterQueue: this.deadLetterQueue, retryAttempts: 2, description: "Sends condition check reminder emails for rentals", } ); // EventBridge Scheduler group this.scheduleGroup = new scheduler.CfnScheduleGroup( this, "ConditionCheckScheduleGroup", { name: `condition-check-reminders-${environment}`, } ); // IAM role for EventBridge Scheduler to invoke Lambda this.schedulerRole = new iam.Role(this, "SchedulerRole", { roleName: `condition-check-scheduler-role-${environment}`, assumedBy: new iam.ServicePrincipal("scheduler.amazonaws.com"), description: "Role for EventBridge Scheduler to invoke Lambda", }); // Allow scheduler to invoke the Lambda this.schedulerRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["lambda:InvokeFunction"], resources: [ this.lambdaFunction.functionArn, `${this.lambdaFunction.functionArn}:*`, ], }) ); // Outputs new cdk.CfnOutput(this, "LambdaFunctionArn", { value: this.lambdaFunction.functionArn, description: "ARN of the Condition Check Reminder Lambda", exportName: `ConditionCheckLambdaArn-${environment}`, }); new cdk.CfnOutput(this, "ScheduleGroupName", { value: this.scheduleGroup.name!, description: "Name of the EventBridge Scheduler group", exportName: `ConditionCheckScheduleGroup-${environment}`, }); new cdk.CfnOutput(this, "SchedulerRoleArn", { value: this.schedulerRole.roleArn, description: "ARN of the EventBridge Scheduler IAM role", exportName: `ConditionCheckSchedulerRoleArn-${environment}`, }); new cdk.CfnOutput(this, "DLQUrl", { value: this.deadLetterQueue.queueUrl, description: "URL of the Dead Letter Queue", exportName: `ConditionCheckDLQUrl-${environment}`, }); } }