257 lines
7.4 KiB
TypeScript
257 lines
7.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 * as ec2 from "aws-cdk-lib/aws-ec2";
|
|
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;
|
|
|
|
/**
|
|
* VPC for Lambda function (required for network isolation)
|
|
*/
|
|
vpc: ec2.IVpc;
|
|
|
|
/**
|
|
* Security group for Lambda function
|
|
*/
|
|
lambdaSecurityGroup: ec2.ISecurityGroup;
|
|
}
|
|
|
|
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,
|
|
vpc,
|
|
lambdaSecurityGroup,
|
|
} = 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}/*`,
|
|
],
|
|
})
|
|
);
|
|
|
|
// VPC permissions - use AWS managed policy for Lambda VPC access
|
|
lambdaRole.addManagedPolicy(
|
|
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
|
"service-role/AWSLambdaVPCAccessExecutionRole"
|
|
)
|
|
);
|
|
|
|
// 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",
|
|
// VPC configuration for network isolation
|
|
vpc,
|
|
vpcSubnets: {
|
|
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
},
|
|
securityGroups: [lambdaSecurityGroup],
|
|
}
|
|
);
|
|
|
|
// 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}`,
|
|
});
|
|
}
|
|
}
|