cdk/lib/update-supporter-plus-amount.ts (167 lines of code) (raw):

import { GuApiLambda } from '@guardian/cdk'; import { GuAlarm } from '@guardian/cdk/lib/constructs/cloudwatch'; import type { GuStackProps } from '@guardian/cdk/lib/constructs/core'; import { GuStack } from '@guardian/cdk/lib/constructs/core'; import type { App } from 'aws-cdk-lib'; import { Duration } from 'aws-cdk-lib'; import { ApiKeySourceType, CfnBasePathMapping, CfnDomainName, } from 'aws-cdk-lib/aws-apigateway'; import { ComparisonOperator, Metric } from 'aws-cdk-lib/aws-cloudwatch'; import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { CfnRecordSet } from 'aws-cdk-lib/aws-route53'; import { nodeVersion } from './node-version'; export interface UpdateSupporterPlusAmountProps extends GuStackProps { stack: string; stage: string; certificateId: string; domainName: string; hostedZoneId: string; } export class UpdateSupporterPlusAmount extends GuStack { constructor(scope: App, id: string, props: UpdateSupporterPlusAmountProps) { super(scope, id, props); const app = 'update-supporter-plus-amount'; const nameWithStage = `${app}-${this.stage}`; const commonEnvironmentVariables = { App: app, Stack: this.stack, Stage: this.stage, }; // ---- API-triggered lambda functions ---- // const lambda = new GuApiLambda(this, `${app}-lambda`, { description: 'An API Gateway triggered lambda to carry out supporter plus amount updates', functionName: nameWithStage, fileName: `${app}.zip`, handler: 'index.handler', runtime: nodeVersion, memorySize: 1024, timeout: Duration.seconds(300), environment: commonEnvironmentVariables, monitoringConfiguration: { noMonitoring: true, }, app: app, api: { id: nameWithStage, restApiName: nameWithStage, description: 'API Gateway created by CDK', proxy: true, deployOptions: { stageName: this.stage, }, apiKeySourceType: ApiKeySourceType.HEADER, defaultMethodOptions: { apiKeyRequired: true, }, }, }); const usagePlan = lambda.api.addUsagePlan('UsagePlan', { name: nameWithStage, description: 'REST endpoints for update-supporter-plus-amount', apiStages: [ { stage: lambda.api.deploymentStage, api: lambda.api, }, ], }); // create api key const apiKey = lambda.api.addApiKey(`${app}-key-${this.stage}`, { apiKeyName: `${app}-key-${this.stage}`, }); // associate api key to plan usagePlan.addApiKey(apiKey); // ---- Alarms ---- // const alarmName = (shortDescription: string) => `update-supporter-plus-amount-${this.stage} ${shortDescription}`; const alarmDescription = (description: string) => `Impact - ${description}. Follow the process in https://docs.google.com/document/d/1_3El3cly9d7u_jPgTcRjLxmdG2e919zCLvmcFCLOYAk/edit`; if (this.stage === 'PROD') { new GuAlarm(this, 'ApiGateway5XXAlarm', { app, alarmName: alarmName( 'Update supporter plus amount - API gateway 5XX response', ), alarmDescription: alarmDescription( 'Update supporter plus amount api returned a 5XX response. This means that a user who was trying to update the ' + 'contribution amount of their supporter plus subscription has received an error. Please check the logs to diagnose the issue', ), evaluationPeriods: 1, threshold: 1, snsTopicName: 'alarms-handler-topic-PROD', comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, metric: new Metric({ metricName: '5XXError', namespace: 'AWS/ApiGateway', statistic: 'Sum', period: Duration.seconds(300), dimensionsMap: { ApiName: nameWithStage, }, }), }); } // ---- 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: lambda.api.restApiId, stage: lambda.api.deploymentStage.stageName, }); new CfnRecordSet(this, 'DNSRecord', { name: props.domainName, type: 'CNAME', hostedZoneId: props.hostedZoneId, ttl: '120', resourceRecords: [cfnDomainName.attrRegionalDomainName], }); const s3InlinePolicy: Policy = new Policy(this, 'S3 inline policy', { statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ['s3:GetObject'], resources: [ `arn:aws:s3::*:membership-dist/${this.stack}/${this.stage}/${app}/`, ], }), ], }); const secretsManagerPolicy: Policy = new Policy( this, 'Secrets Manager policy', { statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ['secretsmanager:GetSecretValue'], resources: [ `arn:aws:secretsmanager:${this.region}:${this.account}:secret:${this.stage}/Zuora-OAuth/SupportServiceLambdas-*`, ], }), ], }, ); const sqsPolicy: Policy = new Policy(this, 'SQS policy', { statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ['sqs:GetQueueUrl', 'sqs:SendMessage'], resources: [ `arn:aws:sqs:${this.region}:${this.account}:braze-emails-${this.stage}`, `arn:aws:sqs:${this.region}:${this.account}:supporter-product-data-${this.stage}`, ], }), ], }); lambda.role?.attachInlinePolicy(s3InlinePolicy); lambda.role?.attachInlinePolicy(secretsManagerPolicy); lambda.role?.attachInlinePolicy(sqsPolicy); } }