Files
rentall-app/infrastructure/cdk/lib/condition-check-lambda-stack.ts
2026-01-13 17:14:19 -05:00

226 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 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
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: ["*"],
})
);
// SES permissions for sending emails
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["ses:SendEmail", "ses:SendRawEmail"],
resources: ["*"],
})
);
// 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}`,
});
}
}