in cdk/lib/support-reminders.ts [30:389]
constructor(scope: App, id: string, props: SupportRemindersProps) {
super(scope, id, props);
// ---- Miscellaneous constants ---- //
const app = "support-reminders";
const vpc = GuVpc.fromIdParameter(this, "vpc");
const alarmsTopic = 'alarms-handler-topic-PROD';
const runtime = Runtime.NODEJS_18_X;
const fileName = "support-reminders.zip";
const environment = {
"Bucket": props.datalakeBucket,
"Stage": this.stage,
};
const securityGroups = [SecurityGroup.fromSecurityGroupId(this, "security-group", props.securityGroupToAccessPostgresId)];
const vpcSubnets = {
subnets: GuVpc.subnetsFromParameter(this),
};
const awsLambdaVpcAccessExecutionRole =
ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole")
// SQS Queues
const queueName = `${app}-queue-${props.stage}`;
const deadLetterQueueName = `dead-letters-${app}-${props.stage}`;
const deadLetterQueue = new Queue(this, `dead-letters-${app}Queue`, {
queueName: deadLetterQueueName,
retentionPeriod: Duration.days(14),
});
new GuAlarm(this, `${app}-alarm`, {
app: app,
snsTopicName: alarmsTopic,
alarmName: `${app}-${this.stage}: failed event on the dead letter queue`,
alarmDescription: `A reminder signup event failed and is now on the dead letter queue.`,
metric: deadLetterQueue
.metric('ApproximateNumberOfMessagesVisible')
.with({ statistic: 'Sum', period: Duration.minutes(1) }),
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
threshold: 0,
evaluationPeriods: 24,
actionsEnabled: this.stage === 'PROD',
});
const queue = new Queue(this, `${app}Queue`, {
queueName,
visibilityTimeout: Duration.minutes(2),
deadLetterQueue: {
// The number of times a message can be unsuccessfully dequeued before being moved to the dead-letter queue.
// This has been set to 1 to avoid duplicate events
maxReceiveCount: 1,
queue: deadLetterQueue,
},
});
// SQS to Lambda event source mapping
const eventSource = new SqsEventSource(queue, {
reportBatchItemFailures: true,
batchSize: 1,
});
const events=[eventSource];
const loggingFormat = LoggingFormat.TEXT;
const sharedLambdaProps = {
app,
runtime,
fileName,
vpc,
vpcSubnets,
securityGroups,
environment,
loggingFormat,
};
const apiRole = new Role(this, 'ApiGatewayToSqsRole', {
assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
});
// grant sqs:SendMessage* to Api Gateway Role
queue.grantSendMessages(apiRole);
// Api Gateway Direct Integration
const sendMessageIntegration = new AwsIntegration({
service: 'sqs',
path: `${this.account}/${queue.queueName}`,
integrationHttpMethod: 'POST',
options: {
credentialsRole: apiRole,
requestParameters: {
'integration.request.header.Content-Type': `'application/x-www-form-urlencoded'`,
},
requestTemplates: {
'application/json': 'Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=X-GU-GeoIP-Country-Code&MessageAttribute.1.Value.DataType=String&MessageAttribute.1.Value.StringValue=$input.params(\'X-GU-GeoIP-Country-Code\')&MessageAttribute.2.Name=EventPath&MessageAttribute.2.Value.DataType=String&MessageAttribute.2.Value.StringValue=$context.path',
},
integrationResponses: [
{
statusCode: '200',
responseTemplates: {
"application/json": `{"done": true}`,
},
},
]
},
});
// ---- API-triggered lambda functions ---- //
const createRemindersSignupLambda = new GuLambdaFunction(this, "create-reminders-signup", {
handler: "create-reminder-signup/lambda/lambda.handler",
functionName: `support-reminders-create-reminder-signup-${this.stage}`,
...sharedLambdaProps,
events,
});
const reactivateRecurringReminderLambda = new GuLambdaFunction(this, "reactivate-recurring-reminder", {
handler: "reactivate-recurring-reminder/lambda/lambda.handler",
functionName: `support-reminders-reactivate-recurring-reminder-${this.stage}`,
...sharedLambdaProps,
});
const cancelRemindersLambda = new GuLambdaFunction(this, "cancel-reminders", {
handler: "cancel-reminders/lambda/lambda.handler",
functionName: `support-reminders-cancel-reminders-${this.stage}`,
...sharedLambdaProps,
});
// ---- API gateway ---- //
const supportRemindersApi = new GuApiGatewayWithLambdaByPath(this, {
app,
defaultCorsPreflightOptions: {
allowOrigins: Cors.ALL_ORIGINS,
allowMethods: Cors.ALL_METHODS,
allowHeaders: ["Content-Type"],
},
monitoringConfiguration: this.stage === 'CODE' ? { noMonitoring: true } : {
snsTopicName: alarmsTopic,
http5xxAlarm: {
tolerated5xxPercentage: 1,
}
},
targets: [
{
path: "/reactivate",
httpMethod: "POST",
lambda: reactivateRecurringReminderLambda,
},
{
path: "/cancel",
httpMethod: "POST",
lambda: cancelRemindersLambda,
},
],
})
// post method to /create
supportRemindersApi.api.root.resourceForPath('/create/one-off').addMethod('POST', sendMessageIntegration, {
methodResponses: [
{
statusCode: '200',
},
],
requestParameters: {
'method.request.header.X-GU-GeoIP-Country-Code': true,
},
requestValidator: new RequestValidator(this, 'one-off-validator', {
restApi: supportRemindersApi.api,
validateRequestParameters: true
}),
});
supportRemindersApi.api.root.resourceForPath('/create/recurring').addMethod('POST', sendMessageIntegration, {
methodResponses: [
{
statusCode: '200',
},
],
requestParameters: {
'method.request.header.X-GU-GeoIP-Country-Code': true,
},
requestValidator: new RequestValidator(this, 'recurring-validator', {
restApi: supportRemindersApi.api,
validateRequestParameters: true
}),
});
// ---- Scheduled lambda functions ---- //
const signupExportsLambda = new GuScheduledLambda(this, "signup-exports", {
handler: "signup-exports/lambda/lambda.handler",
functionName: `support-reminders-signup-exports-${this.stage}`,
rules: [
{
schedule: Schedule.cron({ hour: "00", minute: "05" }),
},
],
monitoringConfiguration: {
snsTopicName: alarmsTopic,
toleratedErrorPercentage: 1,
},
...sharedLambdaProps,
});
const nextRemindersLambda = new GuScheduledLambda(this, "next-reminders", {
handler: "next-reminders/lambda/lambda.handler",
functionName: `support-reminders-next-reminders-${this.stage}`,
rules: [
{
schedule: Schedule.cron({ hour: "00", minute: "05" }),
},
],
monitoringConfiguration: {
snsTopicName: alarmsTopic,
toleratedErrorPercentage: 1,
},
...sharedLambdaProps,
});
// ---- Alarms ---- //
const alarmName = (shortDescription: string) =>
`URGENT 9-5 - ${this.stage} ${shortDescription}`;
const alarmDescription = (description: string) =>
`Impact - ${description}. Follow the process in https://docs.google.com/document/d/1_3El3cly9d7u_jPgTcRjLxmdG2e919zCLvmcFCLOYAk/edit`;
new GuAlarm(this, 'ApiGateway4XXAlarm', {
app,
alarmName: alarmName("API gateway 4XX response"),
alarmDescription: alarmDescription("Reminders API received an invalid request"),
evaluationPeriods: 1,
threshold: 8,
actionsEnabled: isProd(),
snsTopicName: alarmsTopic,
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
metric: new Metric({
metricName: "4XXError",
namespace: "AWS/ApiGateway",
statistic: "Sum",
period: Duration.seconds(300),
dimensionsMap: {
ApiName: `support-reminders-${this.stage}`,
}
}),
});
// ---- DNS ---- //
const certificateArn = `arn:aws:acm:eu-west-1:${this.account}:certificate/${props.certificateId}`;
const cfnDomainName = new CfnDomainName(this, "DomainName", {
domainName: props.domainName,
regionalCertificateArn: certificateArn,
endpointConfiguration: {
types: ["REGIONAL"]
}
});
new CfnBasePathMapping(this, "BasePathMapping", {
domainName: cfnDomainName.ref,
restApiId: supportRemindersApi.api.restApiId,
stage: supportRemindersApi.api.deploymentStage.stageName,
});
new CfnRecordSet(this, "DNSRecord", {
name: props.domainName,
type: "CNAME",
hostedZoneId: props.hostedZoneId,
ttl: "60",
resourceRecords: [
cfnDomainName.attrRegionalDomainName
],
});
// ---- Apply policies ---- //
const ssmInlinePolicy: Policy = new Policy(this, "SSM inline policy", {
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"ssm:GetParametersByPath",
"ssm:GetParameter"
],
resources: [
`arn:aws:ssm:${this.region}:${this.account}:parameter/support-reminders/db-config/${props.stage}`,
`arn:aws:ssm:${this.region}:${this.account}:parameter/support-reminders/idapi/${props.stage}/*`,
`arn:aws:ssm:${this.region}:${this.account}:parameter/${props.stage}/support/support-reminders/db-config`,
`arn:aws:ssm:${this.region}:${this.account}:parameter/${props.stage}/support/support-reminders/idapi/*`,
]
}),
],
})
const s3GetObjectInlinePolicy: Policy = new Policy(this, "S3 getObject inline policy", {
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"s3:GetObject"
],
resources: [
`arn:aws:s3::*:${props.deployBucket}/*`
]
})
],
})
const s3PutObjectInlinePolicy: Policy = new Policy(this, "S3 putObject inline policy", {
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"s3:PutObject",
"s3:PutObjectAcl"
],
resources: [
`arn:aws:s3:::${props.datalakeBucket}`,
`arn:aws:s3:::${props.datalakeBucket}/*`
]
})
]
})
const apiRolePolicy: Policy = new Policy(this, "SendMessagePolicy", {
statements: [
new PolicyStatement({
actions: ["sqs:SendMessage"],
effect: Effect.ALLOW,
resources: [queue.queueArn],
}),
],
})
const apiGatewayTriggeredLambdaFunctions: GuLambdaFunction[] = [
createRemindersSignupLambda,
reactivateRecurringReminderLambda,
cancelRemindersLambda,
]
const scheduledLambdaFunctions: GuLambdaFunction[] = [
signupExportsLambda,
nextRemindersLambda
]
apiGatewayTriggeredLambdaFunctions.forEach((l: GuLambdaFunction) => {
l.role?.addManagedPolicy(awsLambdaVpcAccessExecutionRole)
l.role?.attachInlinePolicy(ssmInlinePolicy)
l.role?.attachInlinePolicy(s3GetObjectInlinePolicy)
l.role?.attachInlinePolicy(apiRolePolicy)
})
scheduledLambdaFunctions.forEach((l: GuLambdaFunction) => {
l.role?.addManagedPolicy(awsLambdaVpcAccessExecutionRole)
l.role?.attachInlinePolicy(ssmInlinePolicy)
l.role?.attachInlinePolicy(s3GetObjectInlinePolicy)
l.role?.attachInlinePolicy(s3PutObjectInlinePolicy)
})
}