cdk/lib/discount-api.ts (162 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 DiscountApiProps extends GuStackProps { stack: string; stage: string; certificateId: string; domainName: string; hostedZoneId: string; } export class DiscountApi extends GuStack { constructor(scope: App, id: string, props: DiscountApiProps) { super(scope, id, props); const app = 'discount-api'; 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: 'A lambda that enables the addition of discounts to existing subscriptions', functionName: nameWithStage, fileName: `${app}.zip`, handler: 'index.handler', runtime: nodeVersion, memorySize: 1024, timeout: Duration.seconds(300), environment: commonEnvironmentVariables, monitoringConfiguration: { noMonitoring: true, // There is a threshold alarm defined below }, 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 discount api', 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); 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}/`, `arn:aws:s3::*:gu-zuora-catalog/PROD/Zuora-${this.stage}/*`, ], }), ], }); 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 sqsEmailPolicy: Policy = new Policy(this, 'SQS email policy', { statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ['sqs:sendmessage'], resources: [ `arn:aws:sqs:${this.region}:${this.account}:braze-emails-${this.stage}`, ], }), ], }); lambda.role?.attachInlinePolicy(s3InlinePolicy); lambda.role?.attachInlinePolicy(secretsManagerPolicy); lambda.role?.attachInlinePolicy(sqsEmailPolicy); // ---- Alarms ---- // const alarmName = (shortDescription: string) => `DISCOUNT-API-${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, 'ApiGateway5XXAlarmCDK', { app, alarmName: alarmName('Discount-api 5XX response'), alarmDescription: alarmDescription( 'Discount api returned a 5XX response check the logs for more information: https://eu-west-1.console.aws.amazon.com/cloudwatch/home?region=eu-west-1#logsV2:log-groups/log-group/$252Faws$252Flambda$252Fdiscount-api-PROD', ), evaluationPeriods: 1, threshold: 1, snsTopicName: 'alarms-handler-topic-PROD', actionsEnabled: this.stage === '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], }); } }