cdk/lib/mobile-save-for-later.ts (137 lines of code) (raw):

import { join } from "path"; import { GuApiGatewayWithLambdaByPath } from "@guardian/cdk"; import type { ApiGatewayAlarms } from "@guardian/cdk"; import type { NoMonitoring } from "@guardian/cdk/lib/constructs/cloudwatch"; import type { GuStackProps } from "@guardian/cdk/lib/constructs/core"; import { GuStack } from "@guardian/cdk/lib/constructs/core"; import { GuLambdaFunction } from "@guardian/cdk/lib/constructs/lambda"; import type { App } from "aws-cdk-lib"; import { Duration } from "aws-cdk-lib"; import { CfnBasePathMapping, CfnDomainName } from "aws-cdk-lib/aws-apigateway"; import { PolicyStatement } from "aws-cdk-lib/aws-iam"; import { Runtime } from "aws-cdk-lib/aws-lambda"; import { CfnRecordSetGroup } from "aws-cdk-lib/aws-route53"; import { CfnInclude } from "aws-cdk-lib/cloudformation-include"; export interface MobileSaveForLaterProps extends GuStackProps { certificateId: string; domainName: string; hostedZoneName: string; hostedZoneId: string; identityApiHost: string; reservedConcurrentExecutions: number; monitoringConfiguration: NoMonitoring | ApiGatewayAlarms; identityOktaIssuerUrl: string; identityOktaAudience: string; } export class MobileSaveForLater extends GuStack { constructor(scope: App, id: string, props: MobileSaveForLaterProps) { super(scope, id, props); const yamlTemplateFilePath = join( __dirname, "../..", "mobile-save-for-later/conf/cfn.yaml" ); const yamlDefinedResources = new CfnInclude(this, "YamlTemplate", { templateFile: yamlTemplateFilePath, }); const app = "mobile-save-for-later"; const commonLambdaProps = { runtime: Runtime.JAVA_21, app, fileName: `${app}.jar`, }; const commonEnvironmentVariables = { App: app, Stack: this.stack, Stage: this.stage, IdentityApiHost: props.identityApiHost, IdentityOktaIssuerUrl: props.identityOktaIssuerUrl, IdentityOktaAudience: props.identityOktaAudience, }; const saveArticlesLambda = new GuLambdaFunction( this, "save-articles-lambda", { handler: "com.gu.sfl.lambda.SaveArticlesLambda::handleRequest", functionName: `mobile-save-for-later-SAVE-cdk-${this.stage}`, timeout: Duration.seconds(60), environment: { ...commonEnvironmentVariables, SavedArticleLimit: "1000", }, ...commonLambdaProps, } ); const fetchArticlesLambda = new GuLambdaFunction( this, "fetch-articles-lambda", { handler: "com.gu.sfl.lambda.FetchArticlesLambda::handleRequest", functionName: `mobile-save-for-later-FETCH-cdk-${this.stage}`, timeout: Duration.seconds(20), reservedConcurrentExecutions: props.reservedConcurrentExecutions, environment: commonEnvironmentVariables, ...commonLambdaProps, } ); [saveArticlesLambda, fetchArticlesLambda].map((lambda) => { lambda.addToRolePolicy( new PolicyStatement({ actions: [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:Query", ], resources: [ yamlDefinedResources .getResource("SaveForLaterDynamoTable") // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#aws-resource-dynamodb-table-return-values .getAtt("Arn") .toString(), ], }) ); }); const saveForLaterApi = new GuApiGatewayWithLambdaByPath(this, { app, restApiName: `${app}-api-${this.stage}`, monitoringConfiguration: props.monitoringConfiguration, targets: [ { path: "/syncedPrefs/me/savedArticles", httpMethod: "POST", lambda: saveArticlesLambda, }, { path: "/syncedPrefs/me", httpMethod: "GET", lambda: fetchArticlesLambda, }, ], }); // N.B. we cannot use GuCertificate here as we deploy to eu-west-1 but the certificate must be created in us-east-1. // https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-edge-optimized-custom-domain-name.html const certificateArn = `arn:aws:acm:us-east-1:${this.account}:certificate/${props.certificateId}`; const cfnDomainName = new CfnDomainName(this, "ApiDomainName", { domainName: props.domainName, certificateArn, securityPolicy: "TLS_1_2", }); new CfnBasePathMapping(this, "ApiMapping", { domainName: cfnDomainName.ref, restApiId: saveForLaterApi.api.restApiId, stage: saveForLaterApi.api.deploymentStage.stageName, }); new CfnRecordSetGroup(this, "ApiRoute53", { hostedZoneId: props.hostedZoneId, recordSets: [ { name: props.domainName, type: "A", aliasTarget: { dnsName: cfnDomainName.attrDistributionDomainName, // This magical value is taken from the AWS docs: // https://docs.amazonaws.cn/en_us/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget-1.html#aws-properties-route53-aliastarget-1-properties hostedZoneId: "Z2FDTNDATAQYW2", }, }, ], }); } }