condition check lambda

This commit is contained in:
jackiettran
2026-01-13 17:14:19 -05:00
parent 2ee5571b5b
commit f5fdcbfb82
30 changed files with 14293 additions and 461 deletions

63
infrastructure/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Rentall Infrastructure
AWS CDK infrastructure for Rentall Lambda functions.
## Prerequisites
- Node.js 20+
- AWS CLI configured with appropriate credentials
- AWS CDK CLI (`npm install -g aws-cdk`)
## Setup
```bash
cd infrastructure/cdk
npm install
```
## Deploy
### Staging
```bash
npm run deploy:staging
```
### Production
```bash
npm run deploy:prod
```
## Environment Variables
The following environment variables should be set before deployment:
- `DATABASE_URL` - PostgreSQL connection string
- `CDK_DEFAULT_ACCOUNT` - AWS account ID
- `CDK_DEFAULT_REGION` - AWS region (defaults to us-east-1)
## Stacks
### ConditionCheckLambdaStack
Creates:
- Lambda function for condition check reminders
- EventBridge Scheduler group for per-rental schedules
- IAM roles for Lambda execution and Scheduler invocation
- Dead letter queue for failed invocations
## Outputs
After deployment, the following values are exported:
- `ConditionCheckLambdaArn-{env}` - Lambda function ARN
- `ConditionCheckScheduleGroup-{env}` - Schedule group name
- `ConditionCheckSchedulerRoleArn-{env}` - Scheduler IAM role ARN
- `ConditionCheckDLQUrl-{env}` - Dead letter queue URL
## Useful Commands
- `npm run synth` - Synthesize CloudFormation template
- `npm run diff` - Compare deployed stack with current state
- `npm run destroy` - Destroy all stacks

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { ConditionCheckLambdaStack } from "../lib/condition-check-lambda-stack";
const app = new cdk.App();
// Get environment from context or default to staging
const environment = app.node.tryGetContext("env") || "staging";
// Environment-specific configurations
const envConfig: Record<
string,
{
databaseUrl: string;
frontendUrl: string;
sesFromEmail: string;
emailEnabled: boolean;
}
> = {
staging: {
// These should be passed via CDK context or SSM parameters in production
databaseUrl:
process.env.DATABASE_URL ||
"postgresql://user:password@localhost:5432/rentall_staging",
frontendUrl: "https://staging.villageshare.app",
sesFromEmail: "noreply@villageshare.app",
emailEnabled: true,
},
prod: {
databaseUrl:
process.env.DATABASE_URL ||
"postgresql://user:password@localhost:5432/rentall_prod",
frontendUrl: "https://villageshare.app",
sesFromEmail: "noreply@villageshare.app",
emailEnabled: true,
},
};
const config = envConfig[environment];
if (!config) {
throw new Error(`Unknown environment: ${environment}`);
}
// Create the Lambda stack
new ConditionCheckLambdaStack(app, `ConditionCheckLambdaStack-${environment}`, {
environment,
databaseUrl: config.databaseUrl,
frontendUrl: config.frontendUrl,
sesFromEmail: config.sesFromEmail,
emailEnabled: config.emailEnabled,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
},
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
tags: {
Environment: environment,
Project: "rentall",
Service: "condition-check-reminder",
},
});

View File

@@ -0,0 +1,21 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws"]
}
}

View File

@@ -0,0 +1,225 @@
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}`,
});
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "rentall-infrastructure",
"version": "1.0.0",
"description": "AWS CDK infrastructure for Rentall Lambda functions",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"cdk": "cdk",
"synth": "cdk synth",
"deploy": "cdk deploy --all",
"deploy:staging": "cdk deploy --all --context env=staging",
"deploy:prod": "cdk deploy --all --context env=prod",
"diff": "cdk diff",
"destroy": "cdk destroy --all"
},
"dependencies": {
"aws-cdk-lib": "^2.170.0",
"constructs": "^10.4.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"aws-cdk": "^2.170.0"
}
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"outDir": "./dist",
"rootDir": ".",
"typeRoots": ["./node_modules/@types"]
},
"include": ["bin/**/*", "lib/**/*"],
"exclude": ["node_modules", "cdk.out"]
}