constructor()

in cdk/lib/batch-email-sender.ts [28:199]


	constructor(scope: App, id: string, props: BatchEmailSenderProps) {
		super(scope, id, props);

		// ---- Miscellaneous constants ---- //
		const isProd = this.stage === 'PROD';
		const app = 'batch-email-sender';
		const alarmTopic = 'alarms-handler-topic-PROD';

		// ---- API-triggered lambda functions ---- //
		const batchEmailSenderLambda = new GuLambdaFunction(
			this,
			'BatchEmailSenderLambda',
			{
				app,
				handler: 'com.gu.batchemailsender.api.batchemail.Handler::apply',
				functionName: `batch-email-sender-${this.stage}`,
				runtime: Runtime.JAVA_21,
				fileName: 'batch-email-sender.jar',
				memorySize: 1536,
				timeout: Duration.seconds(300),
				environment: {
					Stage: this.stage,
					EmailQueueName: Fn.importValue(`comms-${this.stage}-EmailQueueName`),
				},
			},
		);

		// ---- API gateway ---- //
		const batchEmailSenderApi = new GuApiGatewayWithLambdaByPath(this, {
			app,
			defaultCorsPreflightOptions: {
				allowOrigins: Cors.ALL_ORIGINS,
				allowMethods: Cors.ALL_METHODS,
				allowHeaders: ['Content-Type'],
			},
			monitoringConfiguration: { noMonitoring: true },
			targets: [
				{
					path: '/email-batch',
					httpMethod: 'POST',
					lambda: batchEmailSenderLambda,
					apiKeyRequired: true,
				},
			],
		});

		// ---- Usage plan and API key ---- //
		const usagePlan = new UsagePlan(this, 'BatchEmailSenderUsagePlan', {
			name: `batch-email-sender-api-usage-plan-${this.stage}`,
			apiStages: [
				{
					api: batchEmailSenderApi.api,
					stage: batchEmailSenderApi.api.deploymentStage,
				},
			],
		});

		const apiKey = new ApiKey(this, 'BatchEmailSenderApiKey', {
			apiKeyName: `batch-email-sender-api-key-${this.stage}`,
			description: 'Key required to call batch email sender API',
			enabled: true,
		});

		new CfnUsagePlanKey(this, 'BatchEmailSenderUsagePlanKey', {
			keyId: apiKey.keyId,
			keyType: 'API_KEY',
			usagePlanId: usagePlan.usagePlanId,
		});

		// ---- Alarms ---- //
		new GuAlarm(this, 'FailedEmailApiAlarm', {
			app,
			alarmName: `URGENT 9-5 - ${this.stage}: Failed to send email triggered by Salesforce - 5XXError (CDK)`,
			alarmDescription: `API responded with 5xx to Salesforce meaning some emails failed to send. Logs at /aws/lambda/batch-email-sender-${this.stage} repo at https://github.com/guardian/support-service-lambdas/blob/main/handlers/batch-email-sender/`,
			evaluationPeriods: 1,
			threshold: 1,
			actionsEnabled: isProd,
			snsTopicName: alarmTopic,
			comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
			metric: new Metric({
				metricName: '5XXError',
				namespace: 'AWS/ApiGateway',
				statistic: 'Sum',
				period: Duration.seconds(60),
				dimensionsMap: {
					ApiName: `BatchEmailSender-${this.stage}`,
				},
			}),
		});

		new GuAlarm(this, 'FailedEmailLambdaAlarm', {
			app,
			alarmName: `URGENT 9-5 - ${this.stage}: Failed to send email triggered by Salesforce - Lambda crash (CDK)`,
			alarmDescription: `Lambda crashed unexpectedely meaning email message sent from Salesforce to the Service Layer could not be processed. Logs at /aws/lambda/batch-email-sender-${this.stage} repo at https://github.com/guardian/support-service-lambdas/blob/main/handlers/batch-email-sender/`,
			evaluationPeriods: 1,
			threshold: 1,
			actionsEnabled: isProd,
			snsTopicName: alarmTopic,
			comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
			metric: new Metric({
				metricName: 'Errors',
				namespace: 'AWS/Lambda',
				statistic: 'Sum',
				period: Duration.seconds(300),
				dimensionsMap: {
					FunctionName: batchEmailSenderLambda.functionName,
				},
			}),
		});

		// ---- 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: batchEmailSenderApi.api.restApiId,
			stage: batchEmailSenderApi.api.deploymentStage.stageName,
		});

		new CfnRecordSet(this, 'DNSRecord', {
			name: props.domainName,
			type: 'CNAME',
			hostedZoneId: props.hostedZoneId,
			ttl: '60',
			resourceRecords: [cfnDomainName.attrRegionalDomainName],
		});

		// ---- Apply policies ---- //
		const cloudwatchLogsInlinePolicy: Policy = new Policy(
			this,
			'cloudwatch-logs-inline-policy',
			{
				statements: [
					new PolicyStatement({
						effect: Effect.ALLOW,
						actions: [
							'logs:CreateLogGroup',
							'logs:CreateLogStream',
							'logs:PutLogEvents',
							'lambda:InvokeFunction',
						],
						resources: [
							`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/batch-email-sender-${this.stage}:log-stream:*`,
						],
					}),
				],
			},
		);

		const sqsInlinePolicy: Policy = new Policy(this, 'sqs-inline-policy', {
			statements: [
				new PolicyStatement({
					effect: Effect.ALLOW,
					actions: ['sqs:GetQueueUrl', 'sqs:SendMessage'],
					resources: [
						`arn:aws:sqs:${this.region}:${this.account}:braze-emails-${this.stage}`,
					],
				}),
			],
		});

		batchEmailSenderLambda.role?.attachInlinePolicy(cloudwatchLogsInlinePolicy);
		batchEmailSenderLambda.role?.attachInlinePolicy(sqsInlinePolicy);
	}