diff --git a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap index 83b1de944..9a299a5f0 100644 --- a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap +++ b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap @@ -20,6 +20,7 @@ exports[`The ServiceCatalogue stack matches the snapshot 1`] = ` "GuLambdaFunction", "GuLambdaFunction", "GuLambdaFunction", + "GuLambdaErrorPercentageAlarm", ], "gu:cdk:version": "TEST", }, @@ -19237,6 +19238,114 @@ spec: }, "Type": "AWS::Lambda::Function", }, + "obligatronErrorPercentageAlarmForLambda8AFDF23F": { + "Properties": { + "ActionsEnabled": true, + "AlarmActions": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:sns:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":devx-alerts", + ], + ], + }, + ], + "AlarmDescription": { + "Fn::Join": [ + "", + [ + { + "Ref": "obligatronA58CFCF1", + }, + " exceeded 0% error rate", + ], + ], + }, + "AlarmName": { + "Fn::Join": [ + "", + [ + "High error percentage from ", + { + "Ref": "obligatronA58CFCF1", + }, + " lambda in TEST", + ], + ], + }, + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "Metrics": [ + { + "Expression": "100*m1/m2", + "Id": "expr_1", + "Label": { + "Fn::Join": [ + "", + [ + "Error % of ", + { + "Ref": "obligatronA58CFCF1", + }, + ], + ], + }, + }, + { + "Id": "m1", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "obligatronA58CFCF1", + }, + }, + ], + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + }, + "Period": 60, + "Stat": "Sum", + }, + "ReturnData": false, + }, + { + "Id": "m2", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "obligatronA58CFCF1", + }, + }, + ], + "MetricName": "Invocations", + "Namespace": "AWS/Lambda", + }, + "Period": 60, + "Stat": "Sum", + }, + "ReturnData": false, + }, + ], + "Threshold": 0, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, "obligatronServiceRole1E85277A": { "Properties": { "AssumeRolePolicyDocument": { @@ -19433,6 +19542,45 @@ spec: }, "Type": "AWS::IAM::Policy", }, + "obligatronTAGGING3F8E9BB2": { + "Properties": { + "Description": "Daily execution of Obligatron lambda for 'TAGGING' obligation", + "ScheduleExpression": "cron(0 7 * * ? *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "obligatronA58CFCF1", + "Arn", + ], + }, + "Id": "Target0", + "Input": ""TAGGING"", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "obligatronTAGGINGAllowEventRuleServiceCatalogueobligatron5BD5033EC1528439": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "obligatronA58CFCF1", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "obligatronTAGGING3F8E9BB2", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "prismamigratetaskTEST59D4C0E8": { "Properties": { "AssumeRolePolicyDocument": { diff --git a/packages/cdk/lib/obligatron.ts b/packages/cdk/lib/obligatron.ts index 53a740186..bdaf6b70f 100644 --- a/packages/cdk/lib/obligatron.ts +++ b/packages/cdk/lib/obligatron.ts @@ -3,8 +3,11 @@ import type { GuSecurityGroup } from '@guardian/cdk/lib/constructs/ec2'; import { GuLambdaFunction } from '@guardian/cdk/lib/constructs/lambda'; import { Duration } from 'aws-cdk-lib'; import type { IVpc } from 'aws-cdk-lib/aws-ec2'; +import { Rule, RuleTargetInput, Schedule } from 'aws-cdk-lib/aws-events'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; import type { DatabaseInstance } from 'aws-cdk-lib/aws-rds'; +import { Obligations } from '../../obligatron/src/obligations'; type ObligatronProps = { vpc: IVpc; @@ -33,8 +36,24 @@ export class Obligatron { // Unfortunately Prisma doesn't support streaming data from Postgres at the moment https://github.com/prisma/prisma/issues/5055 // This means that all rows need to be loaded into memory at the same time whenever a query is ran hence the high memory requirement. memorySize: 4096, + errorPercentageMonitoring: { + toleratedErrorPercentage: 0, + snsTopicName: 'devx-alerts', + }, }); + for (const obligation of Obligations) { + new Rule(stack, `obligatron-${obligation}`, { + description: `Daily execution of Obligatron lambda for '${obligation}' obligation`, + schedule: Schedule.cron({ minute: '0', hour: '7' }), + targets: [ + new LambdaFunction(lambda, { + event: RuleTargetInput.fromText(obligation), + }), + ], + }); + } + db.grantConnect(lambda, 'obligatron'); } } diff --git a/packages/obligatron/src/index.ts b/packages/obligatron/src/index.ts index b566878c8..2abc15927 100644 --- a/packages/obligatron/src/index.ts +++ b/packages/obligatron/src/index.ts @@ -1,17 +1,12 @@ -import { getPrismaClient } from 'common/database'; import { config } from 'dotenv'; +import { getPrismaClient } from '../../common/src/database'; import { getConfig } from './config'; import type { ObligationResult } from './obligations'; +import { Obligations, stringIsObligation } from './obligations'; import { evaluateTaggingObligation } from './obligations/tagging'; config({ path: `../../.env` }); // Load `.env` file at the root of the repository -const Obligations = ['TAGGING'] as const; -export type Obligation = (typeof Obligations)[number]; -const stringIsObligation = (input: string): input is Obligation => { - return Obligations.filter((v) => v === input).length > 0; -}; - export async function main(obligation: string) { if (!stringIsObligation(obligation)) { throw new Error( diff --git a/packages/obligatron/src/obligations/index.ts b/packages/obligatron/src/obligations/index.ts index 917de45c7..fc23332f7 100644 --- a/packages/obligatron/src/obligations/index.ts +++ b/packages/obligatron/src/obligations/index.ts @@ -1,3 +1,12 @@ +// Slightly hacky file to allow CDK project to import the list of obligations without having to compile the whole Obligatron project + +export const Obligations = ['TAGGING'] as const; +export type Obligation = (typeof Obligations)[number]; + +export const stringIsObligation = (input: string): input is Obligation => { + return Obligations.filter((v) => v === input).length > 0; +}; + export type ObligationResult = { /** * Resource identifier. Varies depending on resource platform.