cdk/lib/zuora-salesforce-link-remover.ts (220 lines of code) (raw):

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 { aws_cloudwatch, Duration } from 'aws-cdk-lib'; import { Alarm, Metric, Stats, TreatMissingData, } from 'aws-cdk-lib/aws-cloudwatch'; import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'; import { Rule, Schedule } from 'aws-cdk-lib/aws-events'; import { SfnStateMachine } from 'aws-cdk-lib/aws-events-targets'; import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Architecture } from 'aws-cdk-lib/aws-lambda'; import { Topic } from 'aws-cdk-lib/aws-sns'; import { Choice, Condition, DefinitionBody, JsonPath, Map, Pass, StateMachine, } from 'aws-cdk-lib/aws-stepfunctions'; import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { nodeVersion } from './node-version'; export class ZuoraSalesforceLinkRemover extends GuStack { constructor(scope: App, id: string, props: GuStackProps) { super(scope, id, props); const appName = 'zuora-salesforce-link-remover'; const allowPutMetric = new PolicyStatement({ effect: Effect.ALLOW, actions: ['cloudwatch:PutMetricData'], resources: ['*'], }); const getSalesforceBillingAccountsLambda = new GuLambdaFunction( this, 'get-billing-accounts-lambda', { app: appName, functionName: `${appName}-get-billing-accounts-${this.stage}`, runtime: nodeVersion, environment: { Stage: this.stage, }, handler: 'getBillingAccounts.handler', fileName: `${appName}.zip`, architecture: Architecture.ARM_64, initialPolicy: [ new PolicyStatement({ actions: ['secretsmanager:GetSecretValue'], resources: [ `arn:aws:secretsmanager:${this.region}:${this.account}:secret:DEV/Salesforce/ConnectedApp/AwsConnectorSandbox-oO8Phf`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:DEV/Salesforce/User/integrationapiuser-rvxxrG`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:PROD/Salesforce/ConnectedApp/BillingAccountRemover-WUdrKa`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:PROD/Salesforce/User/BillingAccountRemoverAPIUser-UJ1SwZ`, ], }), allowPutMetric, ], }, ); const updateZuoraBillingAccountLambda = new GuLambdaFunction( this, 'update-zuora-billing-account-lambda', { app: appName, functionName: `${appName}-update-zuora-billing-account-${this.stage}`, runtime: nodeVersion, environment: { Stage: this.stage, }, handler: 'updateZuoraBillingAccount.handler', fileName: `${appName}.zip`, architecture: Architecture.ARM_64, initialPolicy: [ new PolicyStatement({ actions: ['secretsmanager:GetSecretValue'], resources: [ `arn:aws:secretsmanager:${this.region}:${this.account}:secret:CODE/Zuora-OAuth/SupportServiceLambdas-S8QM4l`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:PROD/Zuora-OAuth/SupportServiceLambdas-Iu3KIT`, ], }), allowPutMetric, ], }, ); const updateSfBillingAccountsLambda = new GuLambdaFunction( this, 'update-sf-billing-accounts-lambda', { app: appName, functionName: `${appName}-update-sf-billing-accounts-${this.stage}`, runtime: nodeVersion, environment: { Stage: this.stage, }, handler: 'updateSfBillingAccounts.handler', fileName: `${appName}.zip`, architecture: Architecture.ARM_64, initialPolicy: [ new PolicyStatement({ actions: ['secretsmanager:GetSecretValue'], resources: [ `arn:aws:secretsmanager:${this.region}:${this.account}:secret:DEV/Salesforce/ConnectedApp/AwsConnectorSandbox-oO8Phf`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:DEV/Salesforce/User/integrationapiuser-rvxxrG`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:PROD/Salesforce/ConnectedApp/BillingAccountRemover-WUdrKa`, `arn:aws:secretsmanager:${this.region}:${this.account}:secret:PROD/Salesforce/User/BillingAccountRemoverAPIUser-UJ1SwZ`, ], }), allowPutMetric, ], }, ); const getSalesforceBillingAccountsFromLambdaTask = new LambdaInvoke( this, 'Get Salesforce Billing Accounts', { lambdaFunction: getSalesforceBillingAccountsLambda, outputPath: '$.Payload', }, ); const updateZuoraBillingAccountLambdaTask = new LambdaInvoke( this, 'Update Zuora Billing Account', { lambdaFunction: updateZuoraBillingAccountLambda, outputPath: '$.Payload', }, ); const updateSfBillingAccountsLambdaTask = new LambdaInvoke( this, 'Update Salesforce Billing Accounts', { lambdaFunction: updateSfBillingAccountsLambda, inputPath: '$.billingAccountProcessingAttempts', outputPath: '$.Payload', }, ); const billingAccountsProcessingMap = new Map( this, 'Billing Accounts Processor Map', { maxConcurrency: 10, itemsPath: JsonPath.stringAt('$.billingAccountsToProcess'), parameters: { item: JsonPath.stringAt('$$.Map.Item.Value'), }, resultPath: '$.billingAccountProcessingAttempts', }, ); billingAccountsProcessingMap.iterator(updateZuoraBillingAccountLambdaTask); const billingAccountsExistChoice = new Choice( this, 'Billing Accounts exist for processing?', ) .when( Condition.isPresent('$.billingAccountsToProcess[0]'), billingAccountsProcessingMap.next(updateSfBillingAccountsLambdaTask), ) .otherwise(new Pass(this, 'No Billing Accounts to process')); const definitionBody = DefinitionBody.fromChainable( getSalesforceBillingAccountsFromLambdaTask.next( billingAccountsExistChoice, ), ); const stateMachine = new StateMachine( this, `zuora-salesforce-link-remover-state-machine-${this.stage}`, { definitionBody: definitionBody, }, ); const cronEveryHour = { minute: '0', hour: '*' }; const cronOncePerYear = { minute: '0', hour: '0', day: '1', month: '1' }; const executionFrequency = this.stage === 'PROD' ? cronEveryHour : cronOncePerYear; new Rule(this, 'ScheduleStateMachineRule', { schedule: Schedule.cron(executionFrequency), targets: [new SfnStateMachine(stateMachine)], enabled: true, }); const topic = Topic.fromTopicArn( this, 'Topic', `arn:aws:sns:${this.region}:${this.account}:alarms-handler-topic-${this.stage}`, ); const lambdaFunctions = [ getSalesforceBillingAccountsLambda, updateZuoraBillingAccountLambda, updateSfBillingAccountsLambda, ]; lambdaFunctions.forEach((lambdaFunction, index) => { const alarm = new Alarm(this, `alarm-${index}`, { alarmName: `Zuora <-> Salesforce link remover - ${lambdaFunction.functionName} - something went wrong - ${this.stage}`, alarmDescription: 'Something went wrong when executing the zuora <-> salesforce link remover. See Cloudwatch logs for more information on the error.', datapointsToAlarm: 1, evaluationPeriods: 1, actionsEnabled: true, comparisonOperator: aws_cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, metric: new Metric({ metricName: 'Errors', namespace: 'AWS/Lambda', statistic: Stats.SUM, period: Duration.seconds(60), dimensionsMap: { FunctionName: lambdaFunction.functionName, }, }), threshold: 0, treatMissingData: TreatMissingData.MISSING, }); alarm.addAlarmAction(new SnsAction(topic)); }); } }