From 5e49430c88a6e8a403fd0f35028d1619d03df8ed Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:46:12 -0400 Subject: [PATCH 1/5] feat(aws-constructs-factories): add cloudwatch alarms to factory output (#1201) * Remove bind statement * Add CW Alarms to State Machine Factory * Update documentation --- .../aws-constructs-factories/README.md | 3 + .../aws-constructs-factories/lib/index.ts | 31 +++- .../integ.facstp-defaults.js.snapshot/cdk.out | 2 +- .../facstp-defaults.assets.json | 6 +- .../facstp-defaults.template.json | 63 +++++++ ...efaultTestDeployAssertFD717592.assets.json | 2 +- .../integ.json | 2 +- .../manifest.json | 22 ++- .../tree.json | 167 +++++++++++++++--- .../state-machine-factory.test.ts | 62 +++++++ .../lib/index.ts | 14 +- .../test/eventbridge-stepfunctions.test.ts | 34 ++++ .../aws-fargate-stepfunctions/lib/index.ts | 14 +- .../test/fargate-stepfunctions.test.ts | 33 ++++ .../aws-lambda-stepfunctions/lib/index.ts | 14 +- .../test/lambda-stepfunctions.test.ts | 60 +++++-- .../lib/openapi-helper.ts | 7 +- .../test/test.openapigateway-lambda.test.ts | 6 +- .../aws-s3-stepfunctions/lib/index.ts | 1 + .../test/s3-stepfunctions.test.ts | 18 ++ .../core/lib/step-function-helper.ts | 68 +++++-- .../core/test/step-function-helper.test.ts | 123 ++++++++++--- .../core/test/utils.test.ts | 1 - 23 files changed, 635 insertions(+), 118 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md index cca150d67..e9658cf6d 100755 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md @@ -171,6 +171,8 @@ stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFa |:-------------|:----------------|-----------------| |stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachineProps.html)|The CDK properties that define the state machine. This property is required and must include a definitionBody or definition (definition is deprecated)| |logGroup?|[]`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroup.html)|An existing LogGroup to which the new state machine will write log entries. Default: none, the construct will create a new log group.| +|createCloudWatchAlarms?|boolean|Whether to create recommended CloudWatch alarms for the State Machine. Default: the alarms are created| +|cloudWatchAlarmsPrefix?|string|Creating multiple State Machines with one Factories construct will result in name collisions as the cloudwatch alarms originally had fixed resource ids. This value was added to avoid collisions while not making changes that would be destructive for existing stacks. Unless you are creating multiple State Machines using factories you can ignore it| # StateMachineFactoryResponse @@ -178,6 +180,7 @@ stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFa |:-------------|:----------------|-----------------| |stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachineProps.html)|| |logGroup|[]`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)|| +|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|The alarms created by the factory (ExecutionFailed, ExecutionThrottled, ExecutionAborted)| # Default settings diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts index 36cc015b7..947e053a9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/lib/index.ts @@ -19,6 +19,7 @@ import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as defaults from '@aws-solutions-constructs/core'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; export interface S3BucketFactoryProps { readonly bucketProps?: s3.BucketProps | any, @@ -44,6 +45,22 @@ export interface StateMachineFactoryProps { * Default: none, the construct will create a new log group. */ readonly logGroupProps?: logs.LogGroupProps + /** + * Whether to create recommended CloudWatch alarms + * + * @default - Alarms are created + */ + readonly createCloudWatchAlarms?: boolean, + /** + * Creating multiple State Machines in 1 stack with constructs will + * result in name collisions as the cloudwatch alarms originally had fixed resource ids. + * This value was added to avoid collisions while not making changes that would be + * destructive for existing stacks. Unless you are creating multiple State Machines using constructs + * you can ignore it. + * + * @default - undefined + */ + readonly cloudWatchAlarmsPrefix?: string } // Create a response specifically for the interface to avoid coupling client with internal implementation @@ -57,6 +74,10 @@ export interface StateMachineFactoryResponse { * The log group that will receive log messages from the state maching */ readonly logGroup: logs.ILogGroup + /* + * The alarms created by the factory (ExecutionFailed, ExecutionThrottled, ExecutionAborted) + */ + readonly cloudwatchAlarms?: cloudwatch.Alarm[]; } export interface SqsQueueFactoryProps { @@ -135,10 +156,16 @@ export class ConstructsFactories extends Construct { } public stateMachineFactory(id: string, props: StateMachineFactoryProps): StateMachineFactoryResponse { - const buildStateMachineResponse = defaults.buildStateMachine(this, id, props.stateMachineProps, props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, id, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + cloudWatchAlarmsPrefix: props.cloudWatchAlarmsPrefix + }); return { stateMachine: buildStateMachineResponse.stateMachine, - logGroup: buildStateMachineResponse.logGroup + logGroup: buildStateMachineResponse.logGroup, + cloudwatchAlarms: buildStateMachineResponse.cloudWatchAlarms }; } diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out index 1f0068d32..ff952427d 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"36.0.25"} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json index 181a77f07..6c83da8db 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.assets.json @@ -1,7 +1,7 @@ { - "version": "36.0.0", + "version": "36.0.25", "files": { - "c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7": { + "c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6": { "source": { "path": "facstp-defaults.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7.json", + "objectKey": "c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json index efc209758..bed066299 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstp-defaults.template.json @@ -176,6 +176,69 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "targetExecutionFailedAlarm97DD1630": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that failed exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsFailed", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "targetExecutionThrottledAlarmCAA85FD5": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that throttled exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionThrottled", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "targetExecutionAbortedAlarm70BA06C6": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm for the number of executions that aborted exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsAborted", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, "lambdatestServiceRoleF3BDB8FC": { "Type": "AWS::IAM::Role", "Properties": { diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json index c89fdb7b7..7659f54db 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.25", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json index 8e67ec928..165f552f6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.25", "testCases": { "facstp-defaults/Integ/DefaultTest": { "stacks": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json index 44a9bbbda..a8be07d8f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "36.0.24", "artifacts": { "facstpdefaultsIntegDefaultTestDeployAssertFD717592.assets": { "type": "cdk:asset-manifest", @@ -66,7 +66,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c7ecd4e8eaccb1e7f7416826559ce7fa1c58bb3a5fb8298e571ac6695d1d50b7.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c609c27e6e7fc30d831454d0ca67932fbbb0f7d66f09883e3fc1e4a0afe294b6.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -106,6 +106,24 @@ "data": "targetStateMachinetestsm59897C57" } ], + "/facstp-defaults/target/ExecutionFailedAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionFailedAlarm97DD1630" + } + ], + "/facstp-defaults/target/ExecutionThrottledAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionThrottledAlarmCAA85FD5" + } + ], + "/facstp-defaults/target/ExecutionAbortedAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetExecutionAbortedAlarm70BA06C6" + } + ], "/facstp-defaults/lambdatest/ServiceRole/Resource": [ { "type": "aws:cdk:logicalId", diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json index aabdf53f6..ec543b73f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/integ.facstp-defaults.js.snapshot/tree.json @@ -47,13 +47,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_logs.CfnLogGroup", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_logs.LogGroup", - "version": "2.150.0" + "version": "2.154.1" } }, "StateMachinetestsm": { @@ -69,7 +69,7 @@ "path": "facstp-defaults/target/StateMachinetestsm/Role/ImportRole", "constructInfo": { "fqn": "aws-cdk-lib.Resource", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -94,7 +94,7 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnRole", - "version": "2.150.0" + "version": "2.154.1" } }, "DefaultPolicy": { @@ -162,19 +162,19 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Policy", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Role", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -227,19 +227,136 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions.CfnStateMachine", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions.StateMachine", - "version": "2.150.0" + "version": "2.154.1" + } + }, + "ExecutionFailedAlarm": { + "id": "ExecutionFailedAlarm", + "path": "facstp-defaults/target/ExecutionFailedAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionFailedAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that failed exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionsFailed", + "namespace": "AWS/States", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" + } + }, + "ExecutionThrottledAlarm": { + "id": "ExecutionThrottledAlarm", + "path": "facstp-defaults/target/ExecutionThrottledAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionThrottledAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that throttled exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionThrottled", + "namespace": "AWS/States", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" + } + }, + "ExecutionAbortedAlarm": { + "id": "ExecutionAbortedAlarm", + "path": "facstp-defaults/target/ExecutionAbortedAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "facstp-defaults/target/ExecutionAbortedAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "alarmDescription": "Alarm for the number of executions that aborted exceeded the threshold of 1. ", + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "dimensions": [ + { + "name": "StateMachineArn", + "value": { + "Ref": "targetStateMachinetestsm59897C57" + } + } + ], + "evaluationPeriods": 1, + "metricName": "ExecutionsAborted", + "namespace": "AWS/States", + "period": 300, + "statistic": "Maximum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "2.154.1" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "2.154.1" } } }, "constructInfo": { "fqn": "@aws-solutions-constructs/aws-constructs-factories.ConstructsFactories", - "version": "2.63.0" + "version": "2.69.0" } }, "lambdatest": { @@ -255,7 +372,7 @@ "path": "facstp-defaults/lambdatest/ServiceRole/ImportServiceRole", "constructInfo": { "fqn": "aws-cdk-lib.Resource", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -294,13 +411,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.CfnRole", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_iam.Role", - "version": "2.150.0" + "version": "2.154.1" } }, "Resource": { @@ -324,13 +441,13 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.aws_lambda.Function", - "version": "2.150.0" + "version": "2.154.1" } }, "tasktest": { @@ -338,7 +455,7 @@ "path": "facstp-defaults/tasktest", "constructInfo": { "fqn": "aws-cdk-lib.aws_stepfunctions_tasks.LambdaInvoke", - "version": "2.150.0" + "version": "2.154.1" } }, "Integ": { @@ -366,7 +483,7 @@ "path": "facstp-defaults/Integ/DefaultTest/DeployAssert/BootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnParameter", - "version": "2.150.0" + "version": "2.154.1" } }, "CheckBootstrapVersion": { @@ -374,25 +491,25 @@ "path": "facstp-defaults/Integ/DefaultTest/DeployAssert/CheckBootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnRule", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.Stack", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", - "version": "2.150.0-alpha.0" + "version": "2.154.1-alpha.0" } } }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", - "version": "2.150.0-alpha.0" + "version": "2.154.1-alpha.0" } }, "BootstrapVersion": { @@ -400,7 +517,7 @@ "path": "facstp-defaults/BootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnParameter", - "version": "2.150.0" + "version": "2.154.1" } }, "CheckBootstrapVersion": { @@ -408,13 +525,13 @@ "path": "facstp-defaults/CheckBootstrapVersion", "constructInfo": { "fqn": "aws-cdk-lib.CfnRule", - "version": "2.150.0" + "version": "2.154.1" } } }, "constructInfo": { "fqn": "aws-cdk-lib.Stack", - "version": "2.150.0" + "version": "2.154.1" } }, "Tree": { @@ -428,7 +545,7 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.App", - "version": "2.150.0" + "version": "2.154.1" } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts index 3f0aa93e3..c9a457e02 100644 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/test/stepfunctions/state-machine-factory.test.ts @@ -78,3 +78,65 @@ test('Existing Log Group', () => { LoggingConfiguration: Match.anyValue() }); }); + +test('check cloudwatch alarm prefix', () => { + const somePrefix = "randomString"; + const stack = new Stack(); + const factories = new ConstructsFactories(stack, 'target'); + const taskOne = new sftasks.EvaluateExpression(stack, 'simpleTask', { + expression: '$.a + $.b' + }); + + const existingLogGroup = new logs.LogGroup(stack, 'existingLogGroup'); + + const startState = sfn.DefinitionBody.fromChainable(taskOne); + + factories.stateMachineFactory('testsm', { + stateMachineProps: { + definitionBody: startState, + logs: { + destination: existingLogGroup + }, + }, + cloudWatchAlarmsPrefix: somePrefix + }); + + const template = Template.fromStack(stack); + const keys = Object.keys((template as any).template.Resources); + const regex = new RegExp(`${somePrefix}Execution`); + const alarms = keys.filter(alarmName => regex.test(alarmName)); + expect(alarms.length).toEqual(3); +}); + +test('Deploy 2 state machines', () => { + const stack = new Stack(); + + const factories = new ConstructsFactories(stack, 'target'); + const taskOne = new sftasks.EvaluateExpression(stack, 'simpleTask', { + expression: '$.a + $.b' + }); + + const startState = sfn.DefinitionBody.fromChainable(taskOne); + + factories.stateMachineFactory('testsm', { + stateMachineProps: { + definitionBody: startState + }, + cloudWatchAlarmsPrefix: "one" + }); + + const taskTwo = new sftasks.EvaluateExpression(stack, 'simpleTaskTwo', { + expression: '$.a + $.b' + }); + + const startStateTwo = sfn.DefinitionBody.fromChainable(taskTwo); + factories.stateMachineFactory('testsm-two', { + stateMachineProps: { + definitionBody: startStateTwo, + }, + cloudWatchAlarmsPrefix: "two" + }); + + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts index 072049f17..eca2b9ac0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/lib/index.ts @@ -80,11 +80,16 @@ export class EventbridgeToStepfunctions extends Construct { constructor(scope: Construct, id: string, props: EventbridgeToStepfunctionsProps) { super(scope, id); defaults.CheckEventBridgeProps(props); + defaults.CheckStateMachineProps(props); - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; // Create an IAM role for Events to start the State Machine const eventsRole = new iam.Role(this, 'EventsRuleRole', { @@ -115,10 +120,5 @@ export class EventbridgeToStepfunctions extends Construct { const eventsRuleProps = overrideProps(defaultEventsRuleProps, props.eventRuleProps, true); // Create the Events Rule for the State Machine this.eventsRule = new events.Rule(this, 'EventsRule', eventsRuleProps); - - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best practices CW Alarms for State Machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts index a57c4fb24..48656a921 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/eventbridge-stepfunctions.test.ts @@ -187,3 +187,37 @@ test('check custom event bus resource with props when deploy:true', () => { Name: 'testcustomeventbus' }); }); + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + + new EventbridgeToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + eventBusProps: { + eventBusName: 'testcustomeventbus' + }, + eventRuleProps: { + eventPattern: { + source: ['solutionsconstructs'] + } + }, + }); + + new EventbridgeToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + eventBusProps: { + eventBusName: 'secondtestcustomeventbus' + }, + eventRuleProps: { + eventPattern: { + source: ['secondsolutionsconstructs'] + } + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts index 7bdc37ab3..fc27e3310 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/lib/index.ts @@ -140,6 +140,7 @@ export class FargateToStepfunctions extends Construct { super(scope, id); defaults.CheckFargateProps(props); defaults.CheckVpcProps(props); + defaults.CheckStateMachineProps(props); this.vpc = defaults.buildVpc(scope, { existingVpc: props.existingVpc, @@ -168,18 +169,17 @@ export class FargateToStepfunctions extends Construct { this.container = createFargateServiceResponse.containerDefinition; } - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; this.stateMachine.grantStartExecution(this.service.taskDefinition.taskRole); - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best-practice CloudWatch Alarm for state machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } - // Add environment variable const stateMachineEnvironmentVariableName = props.stateMachineEnvironmentVariableName || 'STATE_MACHINE_ARN'; this.container.addEnvironment(stateMachineEnvironmentVariableName, this.stateMachine.stateMachineArn); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts index ab94057dd..797ba5e83 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-stepfunctions/test/fargate-stepfunctions.test.ts @@ -354,3 +354,36 @@ function testStateMachineProps(stack: cdk.Stack, userProps?: stepfunctions.State return defaults.consolidateProps(defaultTestProp, userProps); } + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + const existingVpc = defaults.getTestVpc(stack); + + new FargateToStepfunctions(stack, 'test-new-fargate-stepfunctions', { + publicApi: true, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + existingVpc, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new FargateToStepfunctions(stack, 'test-second-fargate-stepfunctions', { + publicApi: true, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + existingVpc, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'secondtest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts index e3379a8ea..0ca52c380 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/lib/index.ts @@ -99,6 +99,7 @@ export class LambdaToStepfunctions extends Construct { super(scope, id); defaults.CheckVpcProps(props); defaults.CheckLambdaProps(props); + defaults.CheckStateMachineProps(props); // Setup vpc if (props.deployVpc || props.existingVpc) { @@ -117,10 +118,14 @@ export class LambdaToStepfunctions extends Construct { } // Setup the state machine - const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, props.stateMachineProps, - props.logGroupProps); + const buildStateMachineResponse = defaults.buildStateMachine(this, defaults.idPlaceholder, { + stateMachineProps: props.stateMachineProps, + logGroupProps: props.logGroupProps, + createCloudWatchAlarms: props.createCloudWatchAlarms, + }); this.stateMachine = buildStateMachineResponse.stateMachine; this.stateMachineLogGroup = buildStateMachineResponse.logGroup; + this.cloudwatchAlarms = buildStateMachineResponse.cloudWatchAlarms; // Setup the Lambda function this.lambdaFunction = defaults.buildLambdaFunction(this, { @@ -135,10 +140,5 @@ export class LambdaToStepfunctions extends Construct { // Grant the start execution permission to the Lambda function this.stateMachine.grantStartExecution(this.lambdaFunction); - - if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { - // Deploy best-practice CloudWatch Alarm for state machine - this.cloudwatchAlarms = defaults.buildStepFunctionCWAlarms(this, this.stateMachine); - } } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts index 9dd9bf11a..4abf9142e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/test/lambda-stepfunctions.test.ts @@ -27,7 +27,7 @@ test('Test deployment with new Lambda function', () => { // Helper declaration new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -57,7 +57,7 @@ test('Test deployment with existing Lambda function', () => { const stack = new Stack(); // Helper declaration const lambdaFunctionProps = { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -88,7 +88,7 @@ test('Test invocation permissions', () => { const stack = new Stack(); // Helper declaration const lambdaFunctionProps = { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -135,7 +135,7 @@ test('Test the properties', () => { // Helper declaration const pattern = new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -164,7 +164,7 @@ test('Test the properties with no CW Alarms', () => { // Helper declaration const pattern = new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -192,7 +192,7 @@ test('Test lambda function custom environment variable', () => { // Helper declaration new LambdaToStepfunctions(stack, 'lambda-to-step-function-stack', { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -223,7 +223,7 @@ test("Test minimal deployment that deploys a VPC without vpcProps", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -274,7 +274,7 @@ test("Test minimal deployment that deploys a VPC w/vpcProps", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -333,7 +333,7 @@ test("Test minimal deployment with an existing VPC", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -375,7 +375,7 @@ test("Test minimal deployment with an existing VPC and existing Lambda function const stack = new Stack(); const testLambdaFunction = new lambda.Function(stack, 'test-lamba', { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: "index.handler", code: lambda.Code.fromAsset(`${__dirname}/lambda`), }); @@ -409,7 +409,7 @@ test("Confirm CheckVpcProps is called", () => { // Helper declaration new LambdaToStepfunctions(stack, "lambda-to-stepfunctions-stack", { lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`) }, @@ -428,7 +428,7 @@ test('Confirm call to CheckLambdaProps', () => { // Initial Setup const stack = new Stack(); const lambdaFunction = new lambda.Function(stack, 'a-function', { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), }); @@ -438,7 +438,7 @@ test('Confirm call to CheckLambdaProps', () => { definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'lamstp-test') }, lambdaFunctionProps: { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), }, @@ -456,7 +456,7 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { const stack = new Stack(); const clientFunction = defaults.deployLambdaFunction(stack, { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda`), environment: { @@ -465,7 +465,7 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { }); const taskFunction = defaults.deployLambdaFunction(stack, { - runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, handler: 'index.handler', code: lambda.Code.fromAsset(`${__dirname}/lambda-task`), environment: { @@ -494,4 +494,32 @@ test('Test deployment a state machine that needs priveleges for tasks', () => { })]) } }); -}); \ No newline at end of file +}); + +test('Deploy 2 constructs', () => { + const stack = new Stack(); + + new LambdaToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + lambdaFunctionProps: { + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new LambdaToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + lambdaFunctionProps: { + runtime: defaults.COMMERCIAL_REGION_LAMBDA_NODE_RUNTIME, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + }, + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts index fea66eb67..72b53ffea 100644 --- a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts +++ b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/lib/openapi-helper.ts @@ -163,8 +163,7 @@ export function ObtainApiDefinition(scope: Construct, props: ObtainApiDefinition apiDefinitionWriter.s3Key ); } else if (apiRawInlineSpec) { - const apiInlineSpec = new apigateway.InlineApiDefinition(apiRawInlineSpec); - newApiDefinition = InlineTemplateWriter(apiInlineSpec.bind(scope), apiIntegrationUris); + newApiDefinition = InlineTemplateWriter(apiRawInlineSpec, apiIntegrationUris); } else { throw new Error("No definition provided (this code should be unreachable)"); } @@ -172,8 +171,8 @@ export function ObtainApiDefinition(scope: Construct, props: ObtainApiDefinition return newApiDefinition!; } -function InlineTemplateWriter({ inlineDefinition }: apigateway.ApiDefinitionConfig, templateValues: resources.TemplateValue[]) { - let template = JSON.stringify(inlineDefinition); +function InlineTemplateWriter(rawInlineSpec: any, templateValues: resources.TemplateValue[]) { + let template = JSON.stringify(rawInlineSpec); // This replicates logic in the template writer custom resource (resources/lib/template-writer-custom-resource/index.ts), // any logic changes should be made to both locations every time diff --git a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts index a95392b90..5ec0592bb 100644 --- a/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-openapigateway-lambda/test/test.openapigateway-lambda.test.ts @@ -625,15 +625,15 @@ test('ObtainApiDefinition from inline JSON spec', () => { tokenToFunctionMap: apiLambdaFunctions }); + const template = Template.fromStack(stack); + template.resourceCountIs("Custom::TemplateWriter", 0); + expect(api).toBeDefined(); expect((api as any).definition).toBeDefined(); expect((api as any).definition.openapi).toEqual("3.0.1"); expect((api as any).definition.info).toBeDefined(); expect((api as any).definition.paths).toBeDefined(); - const template = Template.fromStack(stack); - template.resourceCountIs("Custom::TemplateWriter", 0); - }); test('ObtainApiDefinition from S3 bucket/key', () => { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts index 83fac8004..cd5514293 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts @@ -107,6 +107,7 @@ export class S3ToStepfunctions extends Construct { this.node.setContext("@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy", true); defaults.CheckS3Props(props); + defaults.CheckStateMachineProps(props); let bucket: s3.IBucket; diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts index a3ac429ab..404a983ec 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts @@ -185,3 +185,21 @@ test('s3 bucket with no logging bucket', () => { template.hasResourceProperties("Custom::S3BucketNotifications", {}); expect(construct.s3LoggingBucket).toEqual(undefined); }); + +test('Deploy 2 constructs', () => { + const stack = new cdk.Stack(); + + new S3ToStepfunctions(stack, 'test-new-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + }, + }); + + new S3ToStepfunctions(stack, 'test-second-eventbridge-stepfunctions', { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'seconttest'), + }, + }); + Template.fromStack(stack); + // No checks, as our main concern is this has no collisions +}); diff --git a/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts index 74d4d862e..fdbd51100 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/step-function-helper.ts @@ -31,12 +31,22 @@ import { Construct } from 'constructs'; /* * the id parameter was added to buildStateMachine() long after the original implementation, * this value can be used for the new parameter and ensure behavior is the same. + * (if we just require an id, the state machine name will be changed and it will be a + * destructive change) */ export const idPlaceholder = undefined; +export interface BuildStateMacineProps { + readonly stateMachineProps: sfn.StateMachineProps, + readonly logGroupProps?: logs.LogGroupProps, + readonly createCloudWatchAlarms?: boolean, + readonly cloudWatchAlarmsPrefix?: string +} + export interface BuildStateMachineResponse { readonly stateMachine: sfn.StateMachine, - readonly logGroup: logs.ILogGroup + readonly logGroup: logs.ILogGroup, + readonly cloudWatchAlarms?: cloudwatch.Alarm[] } /** * @internal This is an internal core function and should not be called directly by Solutions Constructs clients. @@ -45,22 +55,21 @@ export interface BuildStateMachineResponse { * @param scope - the construct to which the StateMachine should be attached to. * @param stateMachineProps - user-specified properties to override the default properties. */ -export function buildStateMachine(scope: Construct, id: string | undefined, stateMachineProps: sfn.StateMachineProps, - logGroupProps?: logs.LogGroupProps): BuildStateMachineResponse { +export function buildStateMachine(scope: Construct, id: string | undefined, props: BuildStateMacineProps): BuildStateMachineResponse { let logGroup: logs.ILogGroup; let consolidatedStateMachineProps; // If they sent a logGroup in stateMachineProps - if (stateMachineProps.logs?.destination) { - logGroup = stateMachineProps.logs?.destination; - consolidatedStateMachineProps = stateMachineProps; + if (props.stateMachineProps.logs?.destination) { + logGroup = props.stateMachineProps.logs?.destination; + consolidatedStateMachineProps = props.stateMachineProps; } else { // Three possibilities // 1) logGroupProps not provided - create logGroupProps with just logGroupName // 2) logGroupProps provided with no logGroupName - override logGroupProps.logGroupName // 3) logGroupProps provided with logGroupName - pass unaltered logGroupProps - let consolidatedLogGroupProps = logGroupProps; + let consolidatedLogGroupProps = props.logGroupProps; if (!consolidatedLogGroupProps) { consolidatedLogGroupProps = {}; @@ -84,14 +93,14 @@ export function buildStateMachine(scope: Construct, id: string | undefined, stat logGroup = buildLogGroup(scope, `StateMachineLogGroup${(id ?? '')}`, consolidatedLogGroupProps); // Override the defaults with the user provided props - consolidatedStateMachineProps = overrideProps(smDefaults.DefaultStateMachineProps(logGroup), stateMachineProps); + consolidatedStateMachineProps = overrideProps(smDefaults.DefaultStateMachineProps(logGroup), props.stateMachineProps); } // Override the Cloudwatch permissions to make it more fine grained const newStateMachine = new sfn.StateMachine(scope, `StateMachine${(id ?? '')}`, consolidatedStateMachineProps); // If the client did not pass a role we got the default role and will trim the privileges - if (!stateMachineProps.role) { + if (!props.stateMachineProps.role) { const role = newStateMachine.node.findChild('Role') as iam.Role; const cfnDefaultPolicy = role.node.findChild('DefaultPolicy').node.defaultChild as any; // Override Cfn Nag warning W12: IAM policy should not allow * resource @@ -102,18 +111,26 @@ export function buildStateMachine(scope: Construct, id: string | undefined, stat } ]); } - return { stateMachine: newStateMachine, logGroup }; + const createCloudWatchAlarms: boolean = (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms); + const cloudWatchAlarms = createCloudWatchAlarms ? buildStepFunctionCWAlarms(scope, props.cloudWatchAlarmsPrefix, newStateMachine) : undefined; + + return { + stateMachine: newStateMachine, + logGroup, + cloudWatchAlarms + }; } /** * @internal This is an internal core function and should not be called directly by Solutions Constructs clients. */ -export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine): cloudwatch.Alarm[] { +export function buildStepFunctionCWAlarms(scope: Construct, id: string | undefined, sm: sfn.StateMachine): cloudwatch.Alarm[] { // Setup CW Alarms for State Machine const alarms: cloudwatch.Alarm[] = new Array(); + const prefix = id ?? ""; // Sum of number of executions that failed is >= 1 for 5 minutes, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionFailedAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionFailedAlarm`, { metric: sm.metricFailed({ statistic: 'Sum', period: cdk.Duration.seconds(300), @@ -125,7 +142,7 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); // Sum of number of executions that failed maximum is >= 1 for 5 minute, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionThrottledAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionThrottledAlarm`, { metric: sm.metricThrottled({ statistic: 'Sum', period: cdk.Duration.seconds(300), @@ -137,7 +154,7 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); // Number of executions that aborted maximum is >= 1 for 5 minute, 1 consecutive time - alarms.push(new cloudwatch.Alarm(scope, 'ExecutionAbortedAlarm', { + alarms.push(new cloudwatch.Alarm(scope, `${prefix}ExecutionAbortedAlarm`, { metric: sm.metricAborted({ statistic: 'Maximum', period: cdk.Duration.seconds(300), @@ -149,4 +166,25 @@ export function buildStepFunctionCWAlarms(scope: Construct, sm: sfn.StateMachine })); return alarms; -} \ No newline at end of file +} + +export interface StateMachineProps { + readonly stateMachineProps: sfn.StateMachineProps; + readonly createCloudWatchAlarms?: boolean; + readonly cloudWatchAlarmsPrefix?: string + readonly logGroupProps?: logs.LogGroupProps; +} + +export function CheckStateMachineProps(propsObject: StateMachineProps | any) { + let errorMessages = ''; + let errorFound = false; + + if ((propsObject.createCloudWatchAlarms === false) && propsObject.cloudWatchAlarmsPrefix) { + errorMessages += 'Error - cloudWatchAlarmsPrefix is invalid when createCloudWatchAlarms is false\n'; + errorFound = true; + } + + if (errorFound) { + throw new Error(errorMessages); + } +} diff --git a/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts index f21e3ae7c..937d0d367 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts @@ -28,8 +28,10 @@ test('Test deployment w/ custom properties', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - stateMachineName: 'myStateMachine' + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineName: 'myStateMachine' + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -50,10 +52,12 @@ test('Test deployment w/ logging enabled', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - logs: { - destination: logGroup, - level: sfn.LogLevel.FATAL + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + logs: { + destination: logGroup, + level: sfn.LogLevel.FATAL + } } }); // Assertion @@ -84,7 +88,9 @@ test('Check default Cloudwatch permissions', () => { const stack = new Stack(); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -137,7 +143,9 @@ test('Check State Machine IAM Policy with 2 Lambda fuctions in State Machine Def const startState = sfn.DefinitionBody.fromChainable(taskOne.next(taskTwo)); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: startState + stateMachineProps: { + definitionBody: startState + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -215,7 +223,9 @@ test('Check State Machine IAM Policy with S3 API call in State Machine Definitio // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: sfn.DefinitionBody.fromChainable(stateMachineDefinition) + stateMachineProps: { + definitionBody: sfn.DefinitionBody.fromChainable(stateMachineDefinition) + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -256,12 +266,52 @@ test('Count State Machine CW Alarms', () => { const stack = new Stack(); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + } + }); + expect(buildStateMachineResponse.stateMachine).toBeDefined(); + + expect(buildStateMachineResponse.cloudWatchAlarms!.length).toEqual(3); +}); + +test('Confirm CloudWatch Alarm Prefix is used', () => { + const customPrefix = "SomeText"; + // Stack + const stack = new Stack(); + // Build state machine + const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + }, + cloudWatchAlarmsPrefix: customPrefix + }); + expect(buildStateMachineResponse.stateMachine).toBeDefined(); + + expect(buildStateMachineResponse.cloudWatchAlarms!.length).toEqual(3); + // expect() checks look for properties, not the resource ID, so we need to + // exploit knowledge of the internals of template. This may be brittle, + // take care in the future + const template = Template.fromStack(stack); + const keys = Object.keys((template as any).template.Resources); + const regex = new RegExp(`${customPrefix}Execution`); + const alarms = keys.filter(alarmName => regex.test(alarmName)); + expect(alarms.length).toEqual(3); +}); + +test('Skip creating CW alarms', () => { + // Stack + const stack = new Stack(); + // Build state machine + const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test') + }, + createCloudWatchAlarms: false }); - const cwList = defaults.buildStepFunctionCWAlarms(stack, buildStateMachineResponse.stateMachine); expect(buildStateMachineResponse.stateMachine).toBeDefined(); - expect(cwList.length).toEqual(3); + expect(buildStateMachineResponse.cloudWatchAlarms).not.toBeDefined(); }); test('Test deployment with custom role', () => { @@ -285,8 +335,10 @@ test('Test deployment with custom role', () => { // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), - role: customRole + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + role: customRole + } }); // Assertion @@ -306,8 +358,10 @@ test('Confirm format of name', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineProps: { + stateMachineName: 'myStateMachine', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -331,7 +385,9 @@ test('Confirm format of name with ID provided', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, 'zxz', { - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + stateMachineProps: { + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'test'), + } }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); @@ -354,19 +410,40 @@ test('multiple state machines', () => { const stack = new Stack(undefined, 'teststack'); // Build state machine const buildStateMachineResponse = defaults.buildStateMachine(stack, 'one', { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smOne'), + stateMachineProps: { + stateMachineName: 'myStateMachineOne', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smOne'), + }, + cloudWatchAlarmsPrefix: 'one' }); const buildStateMachineResponseTwo = defaults.buildStateMachine(stack, 'two', { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smTwo'), + stateMachineProps: { + stateMachineName: 'myStateMachineTwo', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smTwo'), + }, + cloudWatchAlarmsPrefix: 'two' }); const buildStateMachineResponseThree = defaults.buildStateMachine(stack, defaults.idPlaceholder, { - stateMachineName: 'myStateMachine', - definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smThree'), + stateMachineProps: { + stateMachineName: 'myStateMachineThree', + definitionBody: defaults.CreateTestStateMachineDefinitionBody(stack, 'smThree'), + }, + cloudWatchAlarmsPrefix: 'three' }); // Assertion expect(buildStateMachineResponse.stateMachine).toBeDefined(); expect(buildStateMachineResponseTwo.stateMachine).toBeDefined(); expect(buildStateMachineResponseThree.stateMachine).toBeDefined(); }); + +test('Confirm cloudWatchAlarmsPrefix requires createCloudWatchAlarms', () => { + + const app = () => { + defaults.CheckStateMachineProps({ + createCloudWatchAlarms: false, + cloudWatchAlarmsPrefix: 'prefix' + }); + }; + // Assertion + expect(app).toThrowError('Error - cloudWatchAlarmsPrefix is invalid when createCloudWatchAlarms is false\n'); +}); diff --git a/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts b/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts index f4c54a638..8d7639868 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/utils.test.ts @@ -338,7 +338,6 @@ test('test addCfnGuardSuppressRules', () => { const template = Template.fromStack(stack); const bucket = template.findResources("AWS::S3::Bucket"); - defaults.printWarning(`DBG*********${JSON.stringify(bucket.testbucketE6E05ABE.Metadata)}`); expect(bucket.testbucketE6E05ABE.Metadata.guard.SuppressedRules[0]).toEqual("ADDED_TO_BUCKET"); expect(bucket.testbucketE6E05ABE.Metadata.guard.SuppressedRules[1]).toEqual("ADDED_TO_CFN_RESOURCE"); }); \ No newline at end of file From da63473993ccb20ebbd59215107c3bae74260048 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:11:09 -0400 Subject: [PATCH 2/5] Add CW Alarms to README.md (#1202) --- .../aws-constructs-factories/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md index e9658cf6d..cb876ee94 100755 --- a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/README.md @@ -190,6 +190,10 @@ Out of the box implementation of the Construct without any override will set the * Configured to log to the new log group at LogLevel.ERROR * Amazon CloudWatch Logs Log Group * Log name is prefaced with /aws/vendedlogs/ to avoid resource policy [issues](https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#cloudwatch-iam-policy). The Log Group name is still created to be unique to the stack to avoid name collisions. +* CloudWatch alarms for: + * 1 or more failed executions + * 1 or more executions being throttled + * 1 or more executions being aborted # Architecture ![Architecture Diagram](sf-architecture.png) From 005ebb339cfd53ff131daafb922c7c5b05667d8d Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:25:17 -0400 Subject: [PATCH 3/5] add alarms (#1203) --- .../sf-architecture.png | Bin 25606 -> 41758 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png b/source/patterns/@aws-solutions-constructs/aws-constructs-factories/sf-architecture.png index 53c15f7ce5b4470dd0aa61c24df72f8cb62158f8..95717d223c6563ae1d59f624a65e05e37ff4f2af 100644 GIT binary patch literal 41758 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 From 540cd885e212133a9e99d88add1fb4f83057b4fe Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Wed, 18 Sep 2024 13:18:39 +0000 Subject: [PATCH 4/5] chore(release): 2.70.0 --- CHANGELOG.md | 7 +++++++ source/lerna.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1625df07c..934e2a3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.70.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.69.0...v2.70.0) (2024-09-18) + + +### Features + +* **aws-constructs-factories:** add cloudwatch alarms to factory output ([#1201](https://github.com/awslabs/aws-solutions-constructs/issues/1201)) ([5e49430](https://github.com/awslabs/aws-solutions-constructs/commit/5e49430c88a6e8a403fd0f35028d1619d03df8ed)) + ## [2.69.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.68.0...v2.69.0) (2024-09-11) Built on CDK v2.154.1 diff --git a/source/lerna.json b/source/lerna.json index df38c2782..44e1bb335 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "2.69.0" + "version": "2.70.0" } From ed36807c88101cc2496b2d06e3bde7b2c53aab89 Mon Sep 17 00:00:00 2001 From: biffgaut Date: Wed, 18 Sep 2024 09:19:43 -0400 Subject: [PATCH 5/5] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 934e2a3fa..f9f908ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. See [standa ## [2.70.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.69.0...v2.70.0) (2024-09-18) +Built on CDK v2.154.1 ### Features