diff --git a/package-lock.json b/package-lock.json index b40a65a221..8f77095f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,6 +181,20 @@ "constructs": "^3.2.0" } }, + "@aws-cdk/aws-cloudwatch-actions": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-cloudwatch-actions/-/aws-cloudwatch-actions-1.86.0.tgz", + "integrity": "sha512-dUFCwinO9JGE/zrh8BoDiwEaPQbHKGpeu3ArJ5u66Adbo02Nzn3C6m5OJB1cJjSPpSyWllm3nybLjVyA5FYQ+A==", + "requires": { + "@aws-cdk/aws-applicationautoscaling": "1.86.0", + "@aws-cdk/aws-autoscaling": "1.86.0", + "@aws-cdk/aws-cloudwatch": "1.86.0", + "@aws-cdk/aws-iam": "1.86.0", + "@aws-cdk/aws-sns": "1.86.0", + "@aws-cdk/core": "1.86.0", + "constructs": "^3.2.0" + } + }, "@aws-cdk/aws-codebuild": { "version": "1.86.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-codebuild/-/aws-codebuild-1.86.0.tgz", diff --git a/package.json b/package.json index 35baa1f16b..cb38637348 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "@aws-cdk/assert": "1.86.0", "@aws-cdk/aws-autoscaling": "1.86.0", + "@aws-cdk/aws-cloudwatch-actions": "1.86.0", "@aws-cdk/aws-ec2": "1.86.0", "@aws-cdk/aws-apigateway": "1.86.0", "@aws-cdk/aws-elasticloadbalancing": "1.86.0", diff --git a/src/constructs/cloudwatch/__snapshots__/lambda-alarms.test.ts.snap b/src/constructs/cloudwatch/__snapshots__/lambda-alarms.test.ts.snap new file mode 100644 index 0000000000..67793394d5 --- /dev/null +++ b/src/constructs/cloudwatch/__snapshots__/lambda-alarms.test.ts.snap @@ -0,0 +1,273 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`The GuLambdaErrorPercentageAlarm pattern should create the correct alarm resource with minimal config 1`] = ` +Object { + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, + "Resources": Object { + "lambda8B5974B5": Object { + "DependsOn": Array [ + "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "lambdaServiceRole494E4CA6", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": "bucket1", + "S3Key": "folder/to/key", + }, + "Handler": "handler.ts", + "MemorySize": 512, + "Role": Object { + "Fn::GetAtt": Array [ + "lambdaServiceRole494E4CA6", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "lambdaServiceRole494E4CA6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "lambdaServiceRoleDefaultPolicyBF6FA5E7": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "Roles": Array [ + Object { + "Ref": "lambdaServiceRole494E4CA6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "mylambdafunction8D341B54": Object { + "Properties": Object { + "AlarmActions": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:sns:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":alerts-topic", + ], + ], + }, + ], + "AlarmDescription": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "lambda8B5974B5", + }, + " exceeded 80% error rate", + ], + ], + }, + "AlarmName": Object { + "Fn::Join": Array [ + "", + Array [ + "High error % from ", + Object { + "Ref": "lambda8B5974B5", + }, + " lambda in ", + Object { + "Ref": "Stage", + }, + ], + ], + }, + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "Metrics": Array [ + Object { + "Expression": "100*m1/m2", + "Id": "expr_1", + "Label": Object { + "Fn::Join": Array [ + "", + Array [ + "Error % of ", + Object { + "Ref": "lambda8B5974B5", + }, + ], + ], + }, + }, + Object { + "Id": "m1", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": Object { + "Ref": "lambda8B5974B5", + }, + }, + ], + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": false, + }, + Object { + "Id": "m2", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": Object { + "Ref": "lambda8B5974B5", + }, + }, + ], + "MetricName": "Invocations", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": false, + }, + ], + "Threshold": 80, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + }, +} +`; diff --git a/src/constructs/cloudwatch/alarm.test.ts b/src/constructs/cloudwatch/alarm.test.ts new file mode 100644 index 0000000000..f25e01d192 --- /dev/null +++ b/src/constructs/cloudwatch/alarm.test.ts @@ -0,0 +1,65 @@ +import "@aws-cdk/assert/jest"; +import { ComparisonOperator } from "@aws-cdk/aws-cloudwatch"; +import { Runtime } from "@aws-cdk/aws-lambda"; +import { simpleGuStackForTesting } from "../../../test/utils"; +import { GuLambdaFunction } from "../lambda"; +import { GuAlarm } from "./alarm"; + +describe("The GuAlarm class", () => { + it("should create a CloudWatch alarm", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + new GuAlarm(stack, "alarm", { + alarmName: `Alarm in ${stack.stage}`, + alarmDescription: "It's broken", + metric: lambda.metricErrors(), + comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + snsTopicName: "alerts-topic", + }); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm"); + }); + + it("should send alerts to the provided SNS Topic", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + new GuAlarm(stack, "alarm", { + alarmName: `Alarm in ${stack.stage}`, + alarmDescription: "It's broken", + metric: lambda.metricErrors(), + comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + snsTopicName: "alerts-topic", + }); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm", { + AlarmActions: [ + { + "Fn::Join": [ + "", + [ + "arn:aws:sns:", + { + Ref: "AWS::Region", + }, + ":", + { + Ref: "AWS::AccountId", + }, + ":alerts-topic", + ], + ], + }, + ], + }); + }); +}); diff --git a/src/constructs/cloudwatch/alarm.ts b/src/constructs/cloudwatch/alarm.ts new file mode 100644 index 0000000000..b0dcdf41c0 --- /dev/null +++ b/src/constructs/cloudwatch/alarm.ts @@ -0,0 +1,19 @@ +import type { AlarmProps } from "@aws-cdk/aws-cloudwatch"; +import { Alarm } from "@aws-cdk/aws-cloudwatch"; +import { SnsAction } from "@aws-cdk/aws-cloudwatch-actions"; +import type { ITopic } from "@aws-cdk/aws-sns"; +import { Topic } from "@aws-cdk/aws-sns"; +import type { GuStack } from "../core"; + +export interface GuAlarmProps extends AlarmProps { + snsTopicName: string; +} + +export class GuAlarm extends Alarm { + constructor(scope: GuStack, id: string, props: GuAlarmProps) { + super(scope, id, props); + const topicArn: string = `arn:aws:sns:${scope.region}:${scope.account}:${props.snsTopicName}`; + const snsTopic: ITopic = Topic.fromTopicArn(scope, "sns-topic-for-alarm-notifications", topicArn); + this.addAlarmAction(new SnsAction(snsTopic)); + } +} diff --git a/src/constructs/cloudwatch/lambda-alarms.test.ts b/src/constructs/cloudwatch/lambda-alarms.test.ts new file mode 100644 index 0000000000..c387f39a02 --- /dev/null +++ b/src/constructs/cloudwatch/lambda-alarms.test.ts @@ -0,0 +1,83 @@ +import "@aws-cdk/assert/jest"; +import { SynthUtils } from "@aws-cdk/assert"; +import { Runtime } from "@aws-cdk/aws-lambda"; +import { simpleGuStackForTesting } from "../../../test/utils"; +import { GuLambdaFunction } from "../lambda"; +import { GuLambdaErrorPercentageAlarm } from "./lambda-alarms"; + +describe("The GuLambdaErrorPercentageAlarm pattern", () => { + it("should create the correct alarm resource with minimal config", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + const props = { + toleratedErrorPercentage: 80, + snsTopicName: "alerts-topic", + lambda: lambda, + }; + new GuLambdaErrorPercentageAlarm(stack, "my-lambda-function", props); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + it("should adjust the number of evaluation periods if a custom value is provided", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + const props = { + toleratedErrorPercentage: 65, + numberOfFiveMinutePeriodsToEvaluate: 12, + snsTopicName: "alerts-topic", + lambda: lambda, + }; + new GuLambdaErrorPercentageAlarm(stack, "my-lambda-function", props); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm", { + EvaluationPeriods: 12, + }); + }); + + it("should use a custom description if one is provided", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + const props = { + toleratedErrorPercentage: 65, + numberOfFiveMinutePeriodsToEvaluate: 12, + snsTopicName: "alerts-topic", + alarmDescription: "test-custom-alarm-description", + lambda: lambda, + }; + new GuLambdaErrorPercentageAlarm(stack, "my-lambda-function", props); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm", { + AlarmDescription: "test-custom-alarm-description", + }); + }); + + it("should use a custom alarm name if one is provided", () => { + const stack = simpleGuStackForTesting(); + const lambda = new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + }); + const props = { + toleratedErrorPercentage: 65, + numberOfFiveMinutePeriodsToEvaluate: 12, + snsTopicName: "alerts-topic", + lambda: lambda, + alarmName: "test-custom-alarm-name", + }; + new GuLambdaErrorPercentageAlarm(stack, "my-lambda-function", props); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm", { + AlarmName: "test-custom-alarm-name", + }); + }); +}); diff --git a/src/constructs/cloudwatch/lambda-alarms.ts b/src/constructs/cloudwatch/lambda-alarms.ts new file mode 100644 index 0000000000..5d27d9c351 --- /dev/null +++ b/src/constructs/cloudwatch/lambda-alarms.ts @@ -0,0 +1,44 @@ +import { ComparisonOperator, MathExpression } from "@aws-cdk/aws-cloudwatch"; +import type { GuStack } from "../core"; +import type { GuLambdaFunction } from "../lambda"; +import type { GuAlarmProps } from "./alarm"; +import { GuAlarm } from "./alarm"; + +export type LambdaMonitoring = NoMonitoring | ErrorPercentageMonitoring; + +export interface NoMonitoring { + noMonitoring: true; +} + +export interface ErrorPercentageMonitoring + extends Omit { + toleratedErrorPercentage: number; + numberOfFiveMinutePeriodsToEvaluate?: number; + noMonitoring?: false; +} + +interface GuLambdaAlarmProps extends ErrorPercentageMonitoring { + lambda: GuLambdaFunction; +} + +export class GuLambdaErrorPercentageAlarm extends GuAlarm { + constructor(scope: GuStack, id: string, props: GuLambdaAlarmProps) { + const mathExpression = new MathExpression({ + expression: "100*m1/m2", + usingMetrics: { m1: props.lambda.metricErrors(), m2: props.lambda.metricInvocations() }, + label: `Error % of ${props.lambda.functionName}`, + }); + const defaultAlarmName = `High error % from ${props.lambda.functionName} lambda in ${scope.stage}`; + const defaultDescription = `${props.lambda.functionName} exceeded ${props.toleratedErrorPercentage}% error rate`; + const alarmProps = { + ...props, + metric: mathExpression, + threshold: props.toleratedErrorPercentage, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + evaluationPeriods: props.numberOfFiveMinutePeriodsToEvaluate ?? 1, + alarmName: props.alarmName ?? defaultAlarmName, + alarmDescription: props.alarmDescription ?? defaultDescription, + }; + super(scope, id, alarmProps); + } +} diff --git a/src/constructs/lambda/lambda.ts b/src/constructs/lambda/lambda.ts index e1e74efaed..47658e0bae 100644 --- a/src/constructs/lambda/lambda.ts +++ b/src/constructs/lambda/lambda.ts @@ -13,7 +13,7 @@ interface ApiProps extends Omit { id: string; } -interface GuFunctionProps extends Omit { +export interface GuFunctionProps extends Omit { code: { bucket: string; key: string }; rules?: Array<{ schedule: Schedule; diff --git a/src/patterns/__snapshots__/scheduled-lambda.test.ts.snap b/src/patterns/__snapshots__/scheduled-lambda.test.ts.snap new file mode 100644 index 0000000000..49d61226ae --- /dev/null +++ b/src/patterns/__snapshots__/scheduled-lambda.test.ts.snap @@ -0,0 +1,512 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`The GuScheduledLambda pattern should create an alarm if monitoring configuration is provided 1`] = ` +Object { + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, + "Resources": Object { + "errorpercentagealarmforscheduledlambdaF6DA7824": Object { + "Properties": Object { + "AlarmActions": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:sns:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":alerts-topic", + ], + ], + }, + ], + "AlarmDescription": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "mylambdafunction8D341B54", + }, + " exceeded 99% error rate", + ], + ], + }, + "AlarmName": Object { + "Fn::Join": Array [ + "", + Array [ + "High error % from ", + Object { + "Ref": "mylambdafunction8D341B54", + }, + " lambda in ", + Object { + "Ref": "Stage", + }, + ], + ], + }, + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "Metrics": Array [ + Object { + "Expression": "100*m1/m2", + "Id": "expr_1", + "Label": Object { + "Fn::Join": Array [ + "", + Array [ + "Error % of ", + Object { + "Ref": "mylambdafunction8D341B54", + }, + ], + ], + }, + }, + Object { + "Id": "m1", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": Object { + "Ref": "mylambdafunction8D341B54", + }, + }, + ], + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": false, + }, + Object { + "Id": "m2", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": Object { + "Ref": "mylambdafunction8D341B54", + }, + }, + ], + "MetricName": "Invocations", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": false, + }, + ], + "Threshold": 99, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "mylambdafunction8D341B54": Object { + "DependsOn": Array [ + "mylambdafunctionServiceRoleDefaultPolicy769897D4", + "mylambdafunctionServiceRoleE82C2E25", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": "test-dist", + "S3Key": "lambda.zip", + }, + "FunctionName": "my-lambda-function", + "Handler": "my-lambda/handler", + "MemorySize": 512, + "Role": Object { + "Fn::GetAtt": Array [ + "mylambdafunctionServiceRoleE82C2E25", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "mylambdafunctionServiceRoleDefaultPolicy769897D4": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-dist", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-dist/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "mylambdafunctionServiceRoleDefaultPolicy769897D4", + "Roles": Array [ + Object { + "Ref": "mylambdafunctionServiceRoleE82C2E25", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "mylambdafunctionServiceRoleE82C2E25": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "mylambdafunctionmylambdafunctionrate1minute06AD0015D": Object { + "Properties": Object { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "mylambdafunction8D341B54", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "mylambdafunctionmylambdafunctionrate1minute0AllowEventRuleTestmylambdafunctionmylambdafunctionrate1minute0A609CD2636B92639": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "mylambdafunction8D341B54", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "mylambdafunctionmylambdafunctionrate1minute06AD0015D", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + }, +} +`; + +exports[`The GuScheduledLambda pattern should create the correct resources with minimal config 1`] = ` +Object { + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, + "Resources": Object { + "mylambdafunction8D341B54": Object { + "DependsOn": Array [ + "mylambdafunctionServiceRoleDefaultPolicy769897D4", + "mylambdafunctionServiceRoleE82C2E25", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": "test-dist", + "S3Key": "lambda.zip", + }, + "FunctionName": "my-lambda-function", + "Handler": "my-lambda/handler", + "MemorySize": 512, + "Role": Object { + "Fn::GetAtt": Array [ + "mylambdafunctionServiceRoleE82C2E25", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "mylambdafunctionServiceRoleDefaultPolicy769897D4": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-dist", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-dist/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "mylambdafunctionServiceRoleDefaultPolicy769897D4", + "Roles": Array [ + Object { + "Ref": "mylambdafunctionServiceRoleE82C2E25", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "mylambdafunctionServiceRoleE82C2E25": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": Array [ + Object { + "Key": "App", + "Value": "testing", + }, + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "mylambdafunctionmylambdafunctionrate1minute06AD0015D": Object { + "Properties": Object { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "mylambdafunction8D341B54", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "mylambdafunctionmylambdafunctionrate1minute0AllowEventRuleTestmylambdafunctionmylambdafunctionrate1minute0A609CD2636B92639": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "mylambdafunction8D341B54", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "mylambdafunctionmylambdafunctionrate1minute06AD0015D", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + }, +} +`; diff --git a/src/patterns/scheduled-lambda.test.ts b/src/patterns/scheduled-lambda.test.ts new file mode 100644 index 0000000000..fa780f18dc --- /dev/null +++ b/src/patterns/scheduled-lambda.test.ts @@ -0,0 +1,43 @@ +import "@aws-cdk/assert/jest"; +import { SynthUtils } from "@aws-cdk/assert"; +import { Schedule } from "@aws-cdk/aws-events"; +import { Runtime } from "@aws-cdk/aws-lambda"; +import { Duration } from "@aws-cdk/core"; +import { simpleGuStackForTesting } from "../../test/utils"; +import type { NoMonitoring } from "../constructs/cloudwatch/lambda-alarms"; +import { GuScheduledLambda } from "./scheduled-lambda"; + +describe("The GuScheduledLambda pattern", () => { + it("should create the correct resources with minimal config", () => { + const stack = simpleGuStackForTesting(); + const noMonitoring: NoMonitoring = { noMonitoring: true }; + const props = { + code: { bucket: "test-dist", key: "lambda.zip" }, + functionName: "my-lambda-function", + handler: "my-lambda/handler", + runtime: Runtime.NODEJS_12_X, + schedule: Schedule.rate(Duration.seconds(60)), + monitoringConfiguration: noMonitoring, + }; + new GuScheduledLambda(stack, "my-lambda-function", props); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + it("should create an alarm if monitoring configuration is provided", () => { + const stack = simpleGuStackForTesting(); + const props = { + code: { bucket: "test-dist", key: "lambda.zip" }, + functionName: "my-lambda-function", + handler: "my-lambda/handler", + runtime: Runtime.NODEJS_12_X, + schedule: Schedule.rate(Duration.seconds(60)), + monitoringConfiguration: { + toleratedErrorPercentage: 99, + snsTopicName: "alerts-topic", + }, + }; + new GuScheduledLambda(stack, "my-lambda-function", props); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + expect(stack).toHaveResource("AWS::CloudWatch::Alarm"); + }); +}); diff --git a/src/patterns/scheduled-lambda.ts b/src/patterns/scheduled-lambda.ts new file mode 100644 index 0000000000..8b297767a4 --- /dev/null +++ b/src/patterns/scheduled-lambda.ts @@ -0,0 +1,27 @@ +import type { Schedule } from "@aws-cdk/aws-events"; +import type { LambdaMonitoring } from "../constructs/cloudwatch/lambda-alarms"; +import { GuLambdaErrorPercentageAlarm } from "../constructs/cloudwatch/lambda-alarms"; +import type { GuStack } from "../constructs/core"; +import { GuLambdaFunction } from "../constructs/lambda"; +import type { GuFunctionProps } from "../constructs/lambda"; + +interface GuScheduledLambdaProps extends Omit { + schedule: Schedule; + monitoringConfiguration: LambdaMonitoring; +} + +export class GuScheduledLambda extends GuLambdaFunction { + constructor(scope: GuStack, id: string, props: GuScheduledLambdaProps) { + const lambdaProps: GuFunctionProps = { + ...props, + rules: [{ schedule: props.schedule }], + }; + super(scope, id, lambdaProps); + if (!props.monitoringConfiguration.noMonitoring) { + new GuLambdaErrorPercentageAlarm(scope, "error-percentage-alarm-for-scheduled-lambda", { + ...props.monitoringConfiguration, + lambda: this, + }); + } + } +}