From 70d1919f5f3ccc5d71a0cd1898a89fd815534e11 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Mon, 2 Jun 2025 22:58:30 +0900 Subject: [PATCH 1/7] add dryrun --- packages/aws-cdk-lib/aws-synthetics/lib/canary.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index b96c492a1c45a..83ad9480d4831 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -293,6 +293,21 @@ export interface CanaryProps { * @default - no kms key if `artifactS3EncryptionMode` is set to `S3_MANAGED`. A key will be created if one is not provided and `artifactS3EncryptionMode` is set to `KMS`. */ readonly artifactS3KmsKey?: kms.IKey; + + /** + * Specifies whether to perform a dry run before updating the canary. + * + * If set to true, CDK will execute a dry run to validate the changes before applying them to the canary. + * If the dry run succeeds, the canary will be updated with the changes. + * If the dry run fails, the CloudFormation deployment will fail with the dry run’s failure reason. + * + * If set to false or omitted, the canary will be updated directly without first performing a dry run. + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/performing-safe-canary-upgrades.html + * + * @default false + */ + readonly dryRunAndUpdate?: boolean; } /** From a9ef2fbd695c8e49afff79fdcf28044c67a6ed4e Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Mon, 2 Jun 2025 23:36:30 +0900 Subject: [PATCH 2/7] add integ --- ...efaultTestDeployAssertAF68FFC6.assets.json | 20 ++ ...aultTestDeployAssertAF68FFC6.template.json | 36 +++ ...ticsCanaryDryRunAndUpdateStack.assets.json | 20 ++ ...csCanaryDryRunAndUpdateStack.template.json | 237 ++++++++++++++++++ .../cdk.out | 1 + .../integ.json | 13 + .../manifest.json | 181 +++++++++++++ .../tree.json | 1 + .../test/integ.canary-dryrun-update.ts | 34 +++ .../aws-cdk-lib/aws-synthetics/lib/canary.ts | 3 +- 10 files changed, 545 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets.json new file mode 100644 index 0000000000000..452a930573b33 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets.json @@ -0,0 +1,20 @@ +{ + "version": "44.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "displayName": "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6 Template", + "source": { + "path": "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json new file mode 100644 index 0000000000000..60d4903f41c72 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json @@ -0,0 +1,20 @@ +{ + "version": "44.0.0", + "files": { + "0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559": { + "displayName": "SyntheticsCanaryDryRunAndUpdateStack Template", + "source": { + "path": "SyntheticsCanaryDryRunAndUpdateStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json new file mode 100644 index 0000000000000..9e47d80b19098 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json @@ -0,0 +1,237 @@ +{ + "Resources": { + "DryRunCanaryArtifactsBucketC5395CB9": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DryRunCanaryArtifactsBucketPolicyCEE0B66A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "DryRunCanaryArtifactsBucketC5395CB9" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "DryRunCanaryArtifactsBucketC5395CB9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DryRunCanaryArtifactsBucketC5395CB9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "DryRunCanaryServiceRoleC0E4F3DB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "DryRunCanaryArtifactsBucketC5395CB9", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DryRunCanaryArtifactsBucketC5395CB9", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "DryRunCanary4A247609": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "DryRunCanaryArtifactsBucketC5395CB9" + } + ] + ] + }, + "Code": { + "Handler": "index.handler", + "Script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "DryRunAndUpdate": true, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "DryRunCanaryServiceRoleC0E4F3DB", + "Arn" + ] + }, + "Name": "dryrun", + "RunConfig": { + "MemoryInMB": 2048, + "TimeoutInSeconds": 240 + }, + "RuntimeVersion": "syn-nodejs-puppeteer-7.0", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)" + }, + "StartCanaryAfterCreation": true + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/cdk.out new file mode 100644 index 0000000000000..b3a26d44a5f73 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"44.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/integ.json new file mode 100644 index 0000000000000..b11fae0a07910 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "44.0.0", + "testCases": { + "SyntheticsCanaryDryRunAndUpdate/DefaultTest": { + "stacks": [ + "SyntheticsCanaryDryRunAndUpdateStack" + ], + "assertionStack": "SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert", + "assertionStackName": "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6" + } + }, + "minimumCliVersion": "2.1017.1" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json new file mode 100644 index 0000000000000..59a86835a01a2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json @@ -0,0 +1,181 @@ +{ + "version": "44.0.0", + "artifacts": { + "SyntheticsCanaryDryRunAndUpdateStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SyntheticsCanaryDryRunAndUpdateStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SyntheticsCanaryDryRunAndUpdateStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SyntheticsCanaryDryRunAndUpdateStack.template.json", + "terminationProtection": false, + "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}/0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SyntheticsCanaryDryRunAndUpdateStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SyntheticsCanaryDryRunAndUpdateStack.assets" + ], + "metadata": { + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "canaryName": "*", + "runtime": "*", + "test": "*", + "memory": "*", + "timeout": "*" + } + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "KMS_MANAGED", + "enforceSSL": true, + "lifecycleRules": "*" + } + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DryRunCanaryArtifactsBucketC5395CB9" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "bucket": "*" + } + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DryRunCanaryArtifactsBucketPolicyCEE0B66A" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "assumedBy": { + "principalAccount": "*", + "assumeRoleAction": "*" + }, + "inlinePolicies": "*", + "managedPolicies": [] + } + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/ImportServiceRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DryRunCanaryServiceRoleC0E4F3DB" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DryRunCanary4A247609" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SyntheticsCanaryDryRunAndUpdateStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SyntheticsCanaryDryRunAndUpdateStack" + }, + "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.template.json", + "terminationProtection": false, + "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}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SyntheticsCanaryDryRunAndUpdateDefaultTestDeployAssertAF68FFC6.assets" + ], + "metadata": { + "/SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + }, + "minimumCliVersion": "2.1017.1" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json new file mode 100644 index 0000000000000..bc7f37278a2fd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"SyntheticsCanaryDryRunAndUpdateStack":{"id":"SyntheticsCanaryDryRunAndUpdateStack","path":"SyntheticsCanaryDryRunAndUpdateStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"DryRunCanary":{"id":"DryRunCanary","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary","constructInfo":{"fqn":"aws-cdk-lib.aws_synthetics.Canary","version":"0.0.0","metadata":[{"canaryName":"*","runtime":"*","test":"*","memory":"*","timeout":"*"}]},"children":{"ArtifactsBucket":{"id":"ArtifactsBucket","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"KMS_MANAGED","enforceSSL":true,"lifecycleRules":"*"}]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"aws:kms"}}]}}}},"Policy":{"id":"Policy","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketPolicy","version":"0.0.0","metadata":[{"bucket":"*"}]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucketPolicy","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::BucketPolicy","aws:cdk:cloudformation:props":{"bucket":{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"},"policyDocument":{"Statement":[{"Action":"s3:*","Condition":{"Bool":{"aws:SecureTransport":"false"}},"Effect":"Deny","Principal":{"AWS":"*"},"Resource":[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}]}],"Version":"2012-10-17"}}}}}}}},"ServiceRole":{"id":"ServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"inlinePolicies":"*","managedPolicies":[]}]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"policies":[{"policyName":"canaryPolicy","policyDocument":{"Statement":[{"Action":"s3:ListAllMyBuckets","Effect":"Allow","Resource":"*"},{"Action":"s3:GetBucketLocation","Effect":"Allow","Resource":{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]}},{"Action":"s3:PutObject","Effect":"Allow","Resource":{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}},{"Action":"cloudwatch:PutMetricData","Condition":{"StringEquals":{"cloudwatch:namespace":"CloudWatchSynthetics"}},"Effect":"Allow","Resource":"*"},{"Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":logs:",{"Ref":"AWS::Region"},":",{"Ref":"AWS::AccountId"},":log-group:/aws/lambda/cwsyn-*"]]}}],"Version":"2012-10-17"}}]}}}}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_synthetics.CfnCanary","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Synthetics::Canary","aws:cdk:cloudformation:props":{"artifactS3Location":{"Fn::Join":["",["s3://",{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"}]]},"code":{"handler":"index.handler","script":"\n exports.handler = async () => {\n console.log('hello world');\n };"},"dryRunAndUpdate":true,"executionRoleArn":{"Fn::GetAtt":["DryRunCanaryServiceRoleC0E4F3DB","Arn"]},"name":"dryrun","runConfig":{"memoryInMb":2048,"timeoutInSeconds":240},"runtimeVersion":"syn-nodejs-puppeteer-7.0","schedule":{"durationInSeconds":"0","expression":"rate(5 minutes)"},"startCanaryAfterCreation":true}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"SyntheticsCanaryDryRunAndUpdate":{"id":"SyntheticsCanaryDryRunAndUpdate","path":"SyntheticsCanaryDryRunAndUpdate","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts new file mode 100644 index 0000000000000..63b603f76996f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts @@ -0,0 +1,34 @@ +import { App, Duration, Size, Stack, StackProps } from 'aws-cdk-lib/core'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { Construct } from 'constructs'; +import * as synthetics from 'aws-cdk-lib/aws-synthetics'; + +class TestStack extends Stack { + public canary: synthetics.Canary; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + this.canary = new synthetics.Canary(this, 'DryRunCanary', { + canaryName: 'dryrun', + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + memory: Size.mebibytes(2048), + timeout: Duration.minutes(4), + dryRunAndUpdate: true, + }); + } +} + +const app = new App(); +const testStack = new TestStack(app, 'SyntheticsCanaryDryRunAndUpdateStack'); + +new IntegTest(app, 'SyntheticsCanaryDryRunAndUpdate', { + testCases: [testStack], +}); diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index 83ad9480d4831..0cca86de0d396 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -305,7 +305,7 @@ export interface CanaryProps { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/performing-safe-canary-upgrades.html * - * @default false + * @default undefined - AWS CloudWatch default is false */ readonly dryRunAndUpdate?: boolean; } @@ -420,6 +420,7 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { ? 'AUTOMATIC' : 'OFF' : undefined, + dryRunAndUpdate: props.dryRunAndUpdate, }); this._resource = resource; From 1484104f487a31035ddb523b79e592f22df35f52 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Tue, 3 Jun 2025 00:04:33 +0900 Subject: [PATCH 3/7] add runtime validation --- .../aws-cdk-lib/aws-synthetics/lib/canary.ts | 54 +++++++++++++++++++ .../aws-synthetics/test/canary.test.ts | 34 ++++++++++++ 2 files changed, 88 insertions(+) diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index 0cca86de0d396..a2f825dd922a0 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -402,6 +402,8 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { this._connections = new ec2.Connections({}); } + this.validateDryRunAndUpdate(props.runtime, props.dryRunAndUpdate); + const resource: CfnCanary = new CfnCanary(this, 'Resource', { artifactS3Location: this.artifactsBucket.s3UrlForObject(props.artifactsBucketLocation?.prefix), executionRoleArn: this.role.roleArn, @@ -433,6 +435,58 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { } } + private validateDryRunAndUpdate(runtime: Runtime, dryRunAndUpdate?: boolean) { + if (dryRunAndUpdate === undefined) { + return; + } + + const runtimeName = runtime.name; + + // supported runtime: syn-nodejs-puppeteer-10.0+, syn-nodejs-playwright-2.0+, syn-python-selenium-5.1+ + const isNodeJsPuppeteer10Plus = !cdk.Token.isUnresolved(runtimeName) && + runtimeName.startsWith('syn-nodejs-puppeteer-') && + this.isVersionGreaterOrEqual(runtimeName.replace('syn-nodejs-puppeteer-', ''), '10.0'); + + const isNodeJsPlaywright2Plus = !cdk.Token.isUnresolved(runtimeName) && + runtimeName.startsWith('syn-nodejs-playwright-') && + this.isVersionGreaterOrEqual(runtimeName.replace('syn-nodejs-playwright-', ''), '2.0'); + + const isPythonSelenium5_1Plus = !cdk.Token.isUnresolved(runtimeName) && + runtimeName.startsWith('syn-python-selenium-') && + this.isVersionGreaterOrEqual(runtimeName.replace('syn-python-selenium-', ''), '5.1'); + + const isSupportedRuntime = isNodeJsPuppeteer10Plus || isNodeJsPlaywright2Plus || isPythonSelenium5_1Plus; + + if (!isSupportedRuntime) { + throw new ValidationError(`dryRunAndUpdate is only supported for canary runtime versions 'syn-nodejs-puppeteer-10.0+', 'syn-nodejs-playwright-2.0+', or 'syn-python-selenium-5.1+', got: ${runtimeName}`, this); + } + } + + /** + * Validates that the version1 is greater than or equal to version2. + * Version format is expected to be 'major.minor.patch'. + * + * @returns true if version1 is greater than or equal to version2, false otherwise. + */ + private isVersionGreaterOrEqual(version1: string, version2: string): boolean { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = i < v1Parts.length ? v1Parts[i] : 0; + const v2Part = i < v2Parts.length ? v2Parts[i] : 0; + + if (v1Part > v2Part) { + return true; + } + if (v1Part < v2Part) { + return false; + } + } + + return true; + } + private cleanupUnderlyingResources() { const provider = AutoDeleteUnderlyingResourcesProvider.getOrCreateProvider(this, AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE, { useCfnResponseWrapper: false, diff --git a/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts b/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts index 934f0f7d45265..351084fdc1109 100644 --- a/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts +++ b/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts @@ -36,6 +36,40 @@ test('Basic canary properties work', () => { }); }); +describe('Performing safe canary updates', () => { + test('configure dryRunAndUpdate', () => { + const stack = new Stack(); + + new synthetics.Canary(stack, 'Canary', { + canaryName: 'mycanary', + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_5_1, + dryRunAndUpdate: true, + }); + }); + + test.each([ + synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_5_0, + synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_9_1, + synthetics.Runtime.SYNTHETICS_NODEJS_PLAYWRIGHT_1_0, + ])('dryRunAndUpdate is not supported for runtime %s', (runtime) => { + const stack = new Stack(); + + expect(() => new synthetics.Canary(stack, 'Canary', { + canaryName: 'mycanary', + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime, + dryRunAndUpdate: true, + })).toThrow(`dryRunAndUpdate is only supported for canary runtime versions 'syn-nodejs-puppeteer-10.0+', 'syn-nodejs-playwright-2.0+', or 'syn-python-selenium-5.1+', got: ${runtime.name}`); + }); +}); + test('Specify handler path for playwright canary', () => { // GIVEN const stack = new Stack(); From 9aa95f3e23e6b4837fb18ca9481e268ea3ddcafc Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Tue, 3 Jun 2025 00:07:37 +0900 Subject: [PATCH 4/7] update snapshot --- ...ticsCanaryDryRunAndUpdateStack.assets.json | 18 +++- ...csCanaryDryRunAndUpdateStack.template.json | 9 +- .../canary.mjs | 87 +++++++++++++++++++ .../nodejs/node_modules/canary.js | 52 +++++++++++ .../nodejs/node_modules/folder/canary.js | 52 +++++++++++ .../playwright/canary.mjs | 87 +++++++++++++++++++ .../python/canary.py | 62 +++++++++++++ .../manifest.json | 29 ++----- .../tree.json | 2 +- .../test/integ.canary-dryrun-update.ts | 10 +-- 10 files changed, 372 insertions(+), 36 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/canary.mjs create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/canary.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/folder/canary.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/playwright/canary.mjs create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/python/canary.py diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json index 60d4903f41c72..b1747d4b5b0bc 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.assets.json @@ -1,7 +1,21 @@ { "version": "44.0.0", "files": { - "0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559": { + "5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f": { + "displayName": "DryRunCanary/Code", + "source": { + "path": "asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "ae7c6a4e014d2b1e3ee1ad38e7664f6c4606a0327578b49d5170f3754e3ed020": { "displayName": "SyntheticsCanaryDryRunAndUpdateStack Template", "source": { "path": "SyntheticsCanaryDryRunAndUpdateStack.template.json", @@ -10,7 +24,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559.json", + "objectKey": "ae7c6a4e014d2b1e3ee1ad38e7664f6c4606a0327578b49d5170f3754e3ed020.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json index 9e47d80b19098..941f86f7dbdea 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/SyntheticsCanaryDryRunAndUpdateStack.template.json @@ -176,8 +176,11 @@ ] }, "Code": { - "Handler": "index.handler", - "Script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + "Handler": "canary.handler", + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f.zip" }, "DryRunAndUpdate": true, "ExecutionRoleArn": { @@ -191,7 +194,7 @@ "MemoryInMB": 2048, "TimeoutInSeconds": 240 }, - "RuntimeVersion": "syn-nodejs-puppeteer-7.0", + "RuntimeVersion": "syn-python-selenium-5.1", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/canary.mjs b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/canary.mjs new file mode 100644 index 0000000000000..eb6bd4e9b823d --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/canary.mjs @@ -0,0 +1,87 @@ +import { URL } from 'url'; +import { synthetics } from '@amzn/synthetics-playwright'; + +const loadBlueprints = async function () { + const urls = [process.env.URL]; + + const browser = await synthetics.launch(); + const browserContext = await browser.newContext(); + let page = await synthetics.newPage(browserContext); + + for (const url of urls) { + await loadUrl(page, url); + } + + // Ensure browser is closed + await synthetics.close(); +}; + +// Reset the page in-between +const resetPage = async function(page) { + try { + // Set page.goto timeout to 30 seconds, adjust as needed + // See https://playwright.dev/docs/api/class-page for page.goto options + await page.goto('about:blank', { waitUntil: 'load', timeout: 30000 }); + } catch (e) { + console.error('Unable to open a blank page. ', e); + } +}; + +const loadUrl = async function (page, url) { + let stepName = null; + let domcontentloaded = false; + + try { + stepName = new URL(url).hostname; + } catch (e) { + const errorString = `Error parsing url: ${url}. ${e}`; + log.error(errorString); + /* If we fail to parse the URL, don't emit a metric with a stepName based on it. + It may not be a legal CloudWatch metric dimension name and we may not have an alarms + setup on the malformed URL stepName. Instead, fail this step which will + show up in the logs and will fail the overall canary and alarm on the overall canary + success rate. + */ + throw e; + }; + + await synthetics.executeStep(stepName, async function () { + try { + /* You can customize the wait condition here. + 'domcontentloaded' - consider operation to be finished when the DOMContentLoaded event is fired. + 'load' - consider operation to be finished when the load event is fired. + 'networkidle' - DISCOURAGED consider operation to be finished when there are no network connections for at least 500 ms. Don't use this method for testing, rely on web assertions to assess readiness instead. + 'commit' - consider operation to be finished when network response is received and the document started loading. + + Set page.goto timeout to 30 seconds, adjust as needed + See https://playwright.dev/docs/api/class-page for page.goto options + */ + const response = await page.goto(url, { waitUntil: 'load', timeout: 30000 }); + if (response) { + domcontentloaded = true; + const status = response.status(); + console.log(`Response status: ${status}`); + + // If the response status code is not a 2xx success code + if (status < 200 || status > 299) { + console.error(`Failed to load url: ${url}, status code: ${status}`); + throw new Error('Failed'); + } + } else { + console.error(`No response returned for url: ${url}`); + throw new Error(logNoResponseString); + } + } catch (e) { + const errorString = `Error navigating to url: ${url}. ${e}`; + console.error(errorString); + throw e; + } + }); + + // Reset page + await resetPage(page); +}; + +export const handler = async (event, context) => { + return await loadBlueprints(); +}; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/canary.js b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/canary.js new file mode 100644 index 0000000000000..d7936811fd8c1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/canary.js @@ -0,0 +1,52 @@ +var synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); +const https = require('https'); +const http = require('http'); + +const apiCanaryBlueprint = async function () { + const postData = ""; + + const verifyRequest = async function (requestOption) { + return new Promise((resolve, reject) => { + log.info("Making request with options: " + JSON.stringify(requestOption)); + let req + if (requestOption.protocol === 'https:') { + req = https.request(requestOption); + } else { + req = http.request(requestOption); + } + req.on('response', (res) => { + log.info(`Status Code: ${res.statusCode}`) + log.info(`Response Headers: ${JSON.stringify(res.headers)}`) + if (res.statusCode !== 200) { + reject("Failed: " + requestOption.pathname); + } + res.on('data', (d) => { + log.info("Response: " + d); + }); + res.on('end', () => { + resolve(); + }) + }); + + req.on('error', (error) => { + reject(error); + }); + + if (postData) { + req.write(postData); + } + req.end(); + }); + } + + const headers = {} + headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' '); + const requestOptions = new URL(process.env.URL); + requestOptions['headers'] = headers; + await verifyRequest(requestOptions); +}; + +exports.handler = async () => { + return await apiCanaryBlueprint(); +}; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/folder/canary.js b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/folder/canary.js new file mode 100644 index 0000000000000..2ee399fbc633f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/nodejs/node_modules/folder/canary.js @@ -0,0 +1,52 @@ +var synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); +const https = require('https'); +const http = require('http'); + +const apiCanaryBlueprint = async function () { + const postData = ""; + + const verifyRequest = async function (requestOption) { + return new Promise((resolve, reject) => { + log.info("Making request with options: " + JSON.stringify(requestOption)); + let req + if (requestOption.protocol === 'https:') { + req = https.request(requestOption); + } else { + req = http.request(requestOption); + } + req.on('response', (res) => { + log.info(`Status Code: ${res.statusCode}`) + log.info(`Response Headers: ${JSON.stringify(res.headers)}`) + if (res.statusCode !== 200) { + reject("Failed: " + requestOption.pathname); + } + res.on('data', (d) => { + log.info("Response: " + d); + }); + res.on('end', () => { + resolve(); + }) + }); + + req.on('error', (error) => { + reject(error); + }); + + if (postData) { + req.write(postData); + } + req.end(); + }); + } + + const headers = {} + headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' '); + const requestOptions = new URL(process.env.URL); + requestOptions['headers'] = headers; + await verifyRequest(requestOptions); +}; + +exports.functionName = async () => { + return await apiCanaryBlueprint(); +}; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/playwright/canary.mjs b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/playwright/canary.mjs new file mode 100644 index 0000000000000..eb6bd4e9b823d --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/playwright/canary.mjs @@ -0,0 +1,87 @@ +import { URL } from 'url'; +import { synthetics } from '@amzn/synthetics-playwright'; + +const loadBlueprints = async function () { + const urls = [process.env.URL]; + + const browser = await synthetics.launch(); + const browserContext = await browser.newContext(); + let page = await synthetics.newPage(browserContext); + + for (const url of urls) { + await loadUrl(page, url); + } + + // Ensure browser is closed + await synthetics.close(); +}; + +// Reset the page in-between +const resetPage = async function(page) { + try { + // Set page.goto timeout to 30 seconds, adjust as needed + // See https://playwright.dev/docs/api/class-page for page.goto options + await page.goto('about:blank', { waitUntil: 'load', timeout: 30000 }); + } catch (e) { + console.error('Unable to open a blank page. ', e); + } +}; + +const loadUrl = async function (page, url) { + let stepName = null; + let domcontentloaded = false; + + try { + stepName = new URL(url).hostname; + } catch (e) { + const errorString = `Error parsing url: ${url}. ${e}`; + log.error(errorString); + /* If we fail to parse the URL, don't emit a metric with a stepName based on it. + It may not be a legal CloudWatch metric dimension name and we may not have an alarms + setup on the malformed URL stepName. Instead, fail this step which will + show up in the logs and will fail the overall canary and alarm on the overall canary + success rate. + */ + throw e; + }; + + await synthetics.executeStep(stepName, async function () { + try { + /* You can customize the wait condition here. + 'domcontentloaded' - consider operation to be finished when the DOMContentLoaded event is fired. + 'load' - consider operation to be finished when the load event is fired. + 'networkidle' - DISCOURAGED consider operation to be finished when there are no network connections for at least 500 ms. Don't use this method for testing, rely on web assertions to assess readiness instead. + 'commit' - consider operation to be finished when network response is received and the document started loading. + + Set page.goto timeout to 30 seconds, adjust as needed + See https://playwright.dev/docs/api/class-page for page.goto options + */ + const response = await page.goto(url, { waitUntil: 'load', timeout: 30000 }); + if (response) { + domcontentloaded = true; + const status = response.status(); + console.log(`Response status: ${status}`); + + // If the response status code is not a 2xx success code + if (status < 200 || status > 299) { + console.error(`Failed to load url: ${url}, status code: ${status}`); + throw new Error('Failed'); + } + } else { + console.error(`No response returned for url: ${url}`); + throw new Error(logNoResponseString); + } + } catch (e) { + const errorString = `Error navigating to url: ${url}. ${e}`; + console.error(errorString); + throw e; + } + }); + + // Reset page + await resetPage(page); +}; + +export const handler = async (event, context) => { + return await loadBlueprints(); +}; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/python/canary.py b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/python/canary.py new file mode 100644 index 0000000000000..fac8b8004a7a7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/asset.5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f/python/canary.py @@ -0,0 +1,62 @@ +# This example comes from the AWS Synthetics service console "API canary" blueprint + +import os +import json +import http.client +import urllib.parse +from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver +from aws_synthetics.common import synthetics_logger as logger + + +def verify_request(method, url, post_data=None, headers={}): + parsed_url = urllib.parse.urlparse(url) + user_agent = str(syn_webdriver.get_canary_user_agent_string()) + if "User-Agent" in headers: + headers["User-Agent"] = " ".join([user_agent, headers["User-Agent"]]) + else: + headers["User-Agent"] = "{}".format(user_agent) + + logger.info("Making request with Method: '%s' URL: %s: Data: %s Headers: %s" % ( + method, url, json.dumps(post_data), json.dumps(headers))) + + if parsed_url.scheme == "https": + conn = http.client.HTTPSConnection(parsed_url.hostname, parsed_url.port) + else: + conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) + + conn.request(method, url, post_data, headers) + response = conn.getresponse() + logger.info("Status Code: %s " % response.status) + logger.info("Response Headers: %s" % json.dumps(response.headers.as_string())) + + if not response.status or response.status < 200 or response.status > 299: + try: + logger.error("Response: %s" % response.read().decode()) + finally: + if response.reason: + conn.close() + raise Exception("Failed: %s" % response.reason) + else: + conn.close() + raise Exception("Failed with status code: %s" % response.status) + + logger.info("Response: %s" % response.read().decode()) + logger.info("HTTP request successfully executed") + conn.close() + + +def main(): + + url = os.environ['URL'] + method = 'GET' + postData = "" + headers = {} + + verify_request(method, url, None, headers) + + logger.info("Canary successfully executed") + + +def handler(event, context): + logger.info("Selenium Python API canary") + main() \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json index 59a86835a01a2..00b509ff6cfd1 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/manifest.json @@ -18,7 +18,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}/0af3c7f2b1384aa6d7175ddf8c7f9b5952d23d6264904bc6c90d24a117ec5559.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ae7c6a4e014d2b1e3ee1ad38e7664f6c4606a0327578b49d5170f3754e3ed020.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -37,23 +37,13 @@ "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary": [ { "type": "aws:cdk:analytics:construct", - "data": { - "canaryName": "*", - "runtime": "*", - "test": "*", - "memory": "*", - "timeout": "*" - } + "data": "*" } ], "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket": [ { "type": "aws:cdk:analytics:construct", - "data": { - "encryption": "KMS_MANAGED", - "enforceSSL": true, - "lifecycleRules": "*" - } + "data": "*" } ], "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Resource": [ @@ -65,9 +55,7 @@ "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy": [ { "type": "aws:cdk:analytics:construct", - "data": { - "bucket": "*" - } + "data": "*" } ], "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy/Resource": [ @@ -79,14 +67,7 @@ "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole": [ { "type": "aws:cdk:analytics:construct", - "data": { - "assumedBy": { - "principalAccount": "*", - "assumeRoleAction": "*" - }, - "inlinePolicies": "*", - "managedPolicies": [] - } + "data": "*" } ], "/SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/ImportServiceRole": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json index bc7f37278a2fd..ee0227cd0ce79 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.js.snapshot/tree.json @@ -1 +1 @@ -{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"SyntheticsCanaryDryRunAndUpdateStack":{"id":"SyntheticsCanaryDryRunAndUpdateStack","path":"SyntheticsCanaryDryRunAndUpdateStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"DryRunCanary":{"id":"DryRunCanary","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary","constructInfo":{"fqn":"aws-cdk-lib.aws_synthetics.Canary","version":"0.0.0","metadata":[{"canaryName":"*","runtime":"*","test":"*","memory":"*","timeout":"*"}]},"children":{"ArtifactsBucket":{"id":"ArtifactsBucket","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"KMS_MANAGED","enforceSSL":true,"lifecycleRules":"*"}]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"aws:kms"}}]}}}},"Policy":{"id":"Policy","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketPolicy","version":"0.0.0","metadata":[{"bucket":"*"}]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucketPolicy","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::BucketPolicy","aws:cdk:cloudformation:props":{"bucket":{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"},"policyDocument":{"Statement":[{"Action":"s3:*","Condition":{"Bool":{"aws:SecureTransport":"false"}},"Effect":"Deny","Principal":{"AWS":"*"},"Resource":[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}]}],"Version":"2012-10-17"}}}}}}}},"ServiceRole":{"id":"ServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"inlinePolicies":"*","managedPolicies":[]}]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"policies":[{"policyName":"canaryPolicy","policyDocument":{"Statement":[{"Action":"s3:ListAllMyBuckets","Effect":"Allow","Resource":"*"},{"Action":"s3:GetBucketLocation","Effect":"Allow","Resource":{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]}},{"Action":"s3:PutObject","Effect":"Allow","Resource":{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}},{"Action":"cloudwatch:PutMetricData","Condition":{"StringEquals":{"cloudwatch:namespace":"CloudWatchSynthetics"}},"Effect":"Allow","Resource":"*"},{"Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":logs:",{"Ref":"AWS::Region"},":",{"Ref":"AWS::AccountId"},":log-group:/aws/lambda/cwsyn-*"]]}}],"Version":"2012-10-17"}}]}}}}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_synthetics.CfnCanary","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Synthetics::Canary","aws:cdk:cloudformation:props":{"artifactS3Location":{"Fn::Join":["",["s3://",{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"}]]},"code":{"handler":"index.handler","script":"\n exports.handler = async () => {\n console.log('hello world');\n };"},"dryRunAndUpdate":true,"executionRoleArn":{"Fn::GetAtt":["DryRunCanaryServiceRoleC0E4F3DB","Arn"]},"name":"dryrun","runConfig":{"memoryInMb":2048,"timeoutInSeconds":240},"runtimeVersion":"syn-nodejs-puppeteer-7.0","schedule":{"durationInSeconds":"0","expression":"rate(5 minutes)"},"startCanaryAfterCreation":true}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"SyntheticsCanaryDryRunAndUpdate":{"id":"SyntheticsCanaryDryRunAndUpdate","path":"SyntheticsCanaryDryRunAndUpdate","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"children":{"SyntheticsCanaryDryRunAndUpdateStack":{"id":"SyntheticsCanaryDryRunAndUpdateStack","path":"SyntheticsCanaryDryRunAndUpdateStack","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"children":{"DryRunCanary":{"id":"DryRunCanary","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":["*"]},"children":{"ArtifactsBucket":{"id":"ArtifactsBucket","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":["*"]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Resource","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"aws:kms"}}]}}}},"Policy":{"id":"Policy","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":["*"]},"children":{"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ArtifactsBucket/Policy/Resource","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::BucketPolicy","aws:cdk:cloudformation:props":{"bucket":{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"},"policyDocument":{"Statement":[{"Action":"s3:*","Condition":{"Bool":{"aws:SecureTransport":"false"}},"Effect":"Deny","Principal":{"AWS":"*"},"Resource":[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}]}],"Version":"2012-10-17"}}}}}}}},"ServiceRole":{"id":"ServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":["*"]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":["*"]}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/ServiceRole/Resource","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"policies":[{"policyName":"canaryPolicy","policyDocument":{"Statement":[{"Action":"s3:ListAllMyBuckets","Effect":"Allow","Resource":"*"},{"Action":"s3:GetBucketLocation","Effect":"Allow","Resource":{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]}},{"Action":"s3:PutObject","Effect":"Allow","Resource":{"Fn::Join":["",[{"Fn::GetAtt":["DryRunCanaryArtifactsBucketC5395CB9","Arn"]},"/*"]]}},{"Action":"cloudwatch:PutMetricData","Condition":{"StringEquals":{"cloudwatch:namespace":"CloudWatchSynthetics"}},"Effect":"Allow","Resource":"*"},{"Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":logs:",{"Ref":"AWS::Region"},":",{"Ref":"AWS::AccountId"},":log-group:/aws/lambda/cwsyn-*"]]}}],"Version":"2012-10-17"}}]}}}}},"Code":{"id":"Code","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Code","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"children":{"Stage":{"id":"Stage","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Code/Stage","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"AssetBucket":{"id":"AssetBucket","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Code/AssetBucket","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2","metadata":[]}}}},"Resource":{"id":"Resource","path":"SyntheticsCanaryDryRunAndUpdateStack/DryRunCanary/Resource","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Synthetics::Canary","aws:cdk:cloudformation:props":{"artifactS3Location":{"Fn::Join":["",["s3://",{"Ref":"DryRunCanaryArtifactsBucketC5395CB9"}]]},"code":{"handler":"canary.handler","s3Bucket":{"Fn::Sub":"cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"},"s3Key":"5178413cfe8db00b2d5dcfa9be417e934c64601d0da3031d88c145c8293bc27f.zip"},"dryRunAndUpdate":true,"executionRoleArn":{"Fn::GetAtt":["DryRunCanaryServiceRoleC0E4F3DB","Arn"]},"name":"dryrun","runConfig":{"memoryInMb":2048,"timeoutInSeconds":240},"runtimeVersion":"syn-python-selenium-5.1","schedule":{"durationInSeconds":"0","expression":"rate(5 minutes)"},"startCanaryAfterCreation":true}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/BootstrapVersion","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdateStack/CheckBootstrapVersion","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}},"SyntheticsCanaryDryRunAndUpdate":{"id":"SyntheticsCanaryDryRunAndUpdate","path":"SyntheticsCanaryDryRunAndUpdate","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SyntheticsCanaryDryRunAndUpdate/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts index 63b603f76996f..0b0bb22e90151 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-dryrun-update.ts @@ -1,3 +1,4 @@ +import * as path from 'node:path'; import { App, Duration, Size, Stack, StackProps } from 'aws-cdk-lib/core'; import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import { Construct } from 'constructs'; @@ -11,13 +12,10 @@ class TestStack extends Stack { this.canary = new synthetics.Canary(this, 'DryRunCanary', { canaryName: 'dryrun', - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_5_1, test: synthetics.Test.custom({ - handler: 'index.handler', - code: synthetics.Code.fromInline(` - exports.handler = async () => { - console.log(\'hello world\'); - };`), + handler: 'canary.handler', + code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), }), memory: Size.mebibytes(2048), timeout: Duration.minutes(4), From 28b6f4a173b7ff70d79eef4eb288076c38a3ca41 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Tue, 3 Jun 2025 00:12:11 +0900 Subject: [PATCH 5/7] add readme --- packages/aws-cdk-lib/aws-synthetics/README.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/aws-cdk-lib/aws-synthetics/README.md b/packages/aws-cdk-lib/aws-synthetics/README.md index 03c7d5eff3dbf..d8c7b847d8b92 100644 --- a/packages/aws-cdk-lib/aws-synthetics/README.md +++ b/packages/aws-cdk-lib/aws-synthetics/README.md @@ -298,6 +298,29 @@ new cloudwatch.Alarm(this, 'CanaryAlarm', { }); ``` +### Performing safe canary updates + +You can configure a canary to first perform a dry run before applying any updates. The `dryRunAndUpdate` property can be used to safely update canaries by validating the changes before they're applied. +This feature is supported for canary runtime versions `syn-nodejs-puppeteer-10.0+`, `syn-nodejs-playwright-2.0+`, and `syn-python-selenium-5.1+`. + +When `dryRunAndUpdate` is set to `true`, CDK will execute a dry run to validate the changes before applying them to the canary. +If the dry run succeeds, the canary will be updated with the changes. +If the dry run fails, the CloudFormation deployment will fail with the dry run's failure reason. + +```ts +const canary = new synthetics.Canary(this, 'MyCanary', { + schedule: synthetics.Schedule.rate(Duration.minutes(5)), + test: synthetics.Test.custom({ + code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), + handler: 'index.handler', + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_5_1, + dryRunAndUpdate: true, // Enable dry run before updating +}); +``` + +For more information, see [Performing safe canary updates](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/performing-safe-canary-upgrades.html). + ### Artifacts You can pass an S3 bucket to store artifacts from canary runs. If you do not, From 9a0a589b68f7884abf87e06cd25bf5b3bd734491 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Jun 2025 17:50:04 +0900 Subject: [PATCH 6/7] Update packages/aws-cdk-lib/aws-synthetics/lib/canary.ts --- packages/aws-cdk-lib/aws-synthetics/lib/canary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index a2f825dd922a0..cf4e8eed50a54 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -436,7 +436,7 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { } private validateDryRunAndUpdate(runtime: Runtime, dryRunAndUpdate?: boolean) { - if (dryRunAndUpdate === undefined) { + if (dryRunAndUpdate !== true) { return; } From b5c567be052cfde9736d1f78d1d979b1e2008919 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 8 Jun 2025 11:17:55 +0900 Subject: [PATCH 7/7] refactor --- .../aws-cdk-lib/aws-synthetics/lib/canary.ts | 52 ++++--------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index cf4e8eed50a54..5c2fc521b7429 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -436,57 +436,25 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { } private validateDryRunAndUpdate(runtime: Runtime, dryRunAndUpdate?: boolean) { - if (dryRunAndUpdate !== true) { + const runtimeName = runtime.name; + if (dryRunAndUpdate !== true || cdk.Token.isUnresolved(runtimeName)) { return; } - const runtimeName = runtime.name; - - // supported runtime: syn-nodejs-puppeteer-10.0+, syn-nodejs-playwright-2.0+, syn-python-selenium-5.1+ - const isNodeJsPuppeteer10Plus = !cdk.Token.isUnresolved(runtimeName) && - runtimeName.startsWith('syn-nodejs-puppeteer-') && - this.isVersionGreaterOrEqual(runtimeName.replace('syn-nodejs-puppeteer-', ''), '10.0'); - - const isNodeJsPlaywright2Plus = !cdk.Token.isUnresolved(runtimeName) && - runtimeName.startsWith('syn-nodejs-playwright-') && - this.isVersionGreaterOrEqual(runtimeName.replace('syn-nodejs-playwright-', ''), '2.0'); - - const isPythonSelenium5_1Plus = !cdk.Token.isUnresolved(runtimeName) && - runtimeName.startsWith('syn-python-selenium-') && - this.isVersionGreaterOrEqual(runtimeName.replace('syn-python-selenium-', ''), '5.1'); + const RUNTIME_REGEX = /^syn-(nodejs-puppeteer|nodejs-playwright|python-selenium)-(\d+\.\d+)$/; + const MIN_SUPPORTED_VERSIONS: { [family: string]: number } = { + 'nodejs-puppeteer': 10.0, + 'nodejs-playwright': 2.0, + 'python-selenium': 5.1, + }; - const isSupportedRuntime = isNodeJsPuppeteer10Plus || isNodeJsPlaywright2Plus || isPythonSelenium5_1Plus; + const match = runtimeName.match(RUNTIME_REGEX); - if (!isSupportedRuntime) { + if (!match || match.length < 3 || MIN_SUPPORTED_VERSIONS[match[1]] === undefined || parseFloat(match[2]) < MIN_SUPPORTED_VERSIONS[match[1]]) { throw new ValidationError(`dryRunAndUpdate is only supported for canary runtime versions 'syn-nodejs-puppeteer-10.0+', 'syn-nodejs-playwright-2.0+', or 'syn-python-selenium-5.1+', got: ${runtimeName}`, this); } } - /** - * Validates that the version1 is greater than or equal to version2. - * Version format is expected to be 'major.minor.patch'. - * - * @returns true if version1 is greater than or equal to version2, false otherwise. - */ - private isVersionGreaterOrEqual(version1: string, version2: string): boolean { - const v1Parts = version1.split('.').map(Number); - const v2Parts = version2.split('.').map(Number); - - for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { - const v1Part = i < v1Parts.length ? v1Parts[i] : 0; - const v2Part = i < v2Parts.length ? v2Parts[i] : 0; - - if (v1Part > v2Part) { - return true; - } - if (v1Part < v2Part) { - return false; - } - } - - return true; - } - private cleanupUnderlyingResources() { const provider = AutoDeleteUnderlyingResourcesProvider.getOrCreateProvider(this, AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE, { useCfnResponseWrapper: false,