diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json index 19ddf51ac30c3..0e8b4357eaa36 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json @@ -1,7 +1,7 @@ { - "version": "32.0.0", + "version": "34.0.0", "files": { - "0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5": { + "92fad8a62da258c4bcae7ecf5ebdde433e9614aec519088b7a03a22067623d5e": { "source": { "path": "cdk-backup.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5.json", + "objectKey": "92fad8a62da258c4bcae7ecf5ebdde433e9614aec519088b7a03a22067623d5e.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-backup/test/integ.backup.js.snapshot/cdk-backup.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json index 2722b98da789d..e283f77f3a8f8 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json @@ -142,6 +142,17 @@ "BackupVaultName" ] } + }, + { + "RuleName": "PlanRule4", + "ScheduleExpression": "cron(0 4 15 * ? *)", + "ScheduleExpressionTimezone": "Europe/Brussels", + "TargetBackupVault": { + "Fn::GetAtt": [ + "Vault23237E5B", + "BackupVaultName" + ] + } } ] } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out index f0b901e7c06e5..2313ab5436501 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"32.0.0"} \ No newline at end of file +{"version":"34.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json index 266124ac58c12..b044eefcaffbb 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "33.0.0", + "version": "34.0.0", "testCases": { "integ.backup": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json index aa4d73d1faac8..0187382f487d1 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "33.0.0", + "version": "34.0.0", "artifacts": { "cdk-backup.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,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}/0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/92fad8a62da258c4bcae7ecf5ebdde433e9614aec519088b7a03a22067623d5e.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json index 3728b496ca8cf..eef9deb001c03 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json @@ -37,22 +37,22 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", + "version": "0.0.0" } }, "ScalingRole": { "id": "ScalingRole", "path": "cdk-backup/Table/ScalingRole", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0" } }, "FileSystem": { @@ -63,8 +63,8 @@ "aws:cdk:cloudformation:props": {} }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_efs.CfnFileSystem", + "version": "0.0.0" } }, "Vault": { @@ -84,14 +84,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "SecondaryVault": { @@ -111,22 +111,22 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "Env": { "id": "Env", "path": "cdk-backup/Env", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" } }, "ThirdVault": { @@ -156,14 +156,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "Plan": { @@ -244,14 +244,25 @@ "recoveryPointTags": { "stage": "prod" } + }, + { + "ruleName": "PlanRule4", + "scheduleExpression": "cron(0 4 15 * ? *)", + "scheduleExpressionTimezone": "Europe/Brussels", + "targetBackupVault": { + "Fn::GetAtt": [ + "Vault23237E5B", + "BackupVaultName" + ] + } } ] } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupPlan", + "version": "0.0.0" } }, "Selection": { @@ -266,8 +277,8 @@ "id": "ImportRole", "path": "cdk-backup/Plan/Selection/Role/ImportRole", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" } }, "Resource": { @@ -305,14 +316,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" } }, "Resource": { @@ -394,42 +405,42 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupSelection", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupSelection", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupPlan", + "version": "0.0.0" } }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "cdk-backup/BootstrapVersion", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "cdk-backup/CheckBootstrapVersion", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" } }, "Tree": { @@ -437,13 +448,13 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.69" + "version": "10.2.70" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.ts index d5138ced8bc41..62a3bca47e1f4 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.ts @@ -1,5 +1,6 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as efs from 'aws-cdk-lib/aws-efs'; +import * as events from 'aws-cdk-lib/aws-events'; import { App, Duration, RemovalPolicy, Stack, StackProps, CfnParameter } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as backup from 'aws-cdk-lib/aws-backup'; @@ -61,6 +62,15 @@ class TestStack extends Stack { stage: 'prod', }, })); + + plan.addRule(new backup.BackupPlanRule({ + scheduleExpression: events.Schedule.cron({ + day: '15', + hour: '4', + minute: '0', + }), + scheduleExpressionTimezone: 'Europe/Brussels', + })); } } diff --git a/packages/aws-cdk-lib/aws-backup/README.md b/packages/aws-cdk-lib/aws-backup/README.md index 27b2fcc3667f1..e78fd24c7c6b5 100644 --- a/packages/aws-cdk-lib/aws-backup/README.md +++ b/packages/aws-cdk-lib/aws-backup/README.md @@ -71,6 +71,7 @@ plan.addRule(new backup.BackupPlanRule({ hour: '3', minute: '30', }), + scheduleExpressionTimezone: 'Europe/Brussels', // Optional, defaults to UTC moveToColdStorageAfter: Duration.days(30), })); ``` diff --git a/packages/aws-cdk-lib/aws-backup/lib/plan.ts b/packages/aws-cdk-lib/aws-backup/lib/plan.ts index a7ce167ac92ea..99b102d5e09e6 100644 --- a/packages/aws-cdk-lib/aws-backup/lib/plan.ts +++ b/packages/aws-cdk-lib/aws-backup/lib/plan.ts @@ -188,6 +188,7 @@ export class BackupPlan extends Resource implements IBackupPlan { }, ruleName: rule.props.ruleName ?? `${this.node.id}Rule${this.rules.length}`, scheduleExpression: rule.props.scheduleExpression?.expressionString, + scheduleExpressionTimezone: rule.props.scheduleExpressionTimezone, startWindowMinutes: rule.props.startWindow?.toMinutes(), enableContinuousBackup: rule.props.enableContinuousBackup, targetBackupVault: vault.backupVaultName, diff --git a/packages/aws-cdk-lib/aws-backup/lib/rule.ts b/packages/aws-cdk-lib/aws-backup/lib/rule.ts index fe546619bddb3..663f48af6d10e 100644 --- a/packages/aws-cdk-lib/aws-backup/lib/rule.ts +++ b/packages/aws-cdk-lib/aws-backup/lib/rule.ts @@ -44,6 +44,17 @@ export interface BackupPlanRuleProps { */ readonly scheduleExpression?: events.Schedule; + /** + * The name of the time zone for the schedule expression, following the IANA time zone standard. + * + * @example America/Los_Angeles + * + * @see https://www.iana.org/time-zones + * + * @default - UTC + */ + readonly scheduleExpressionTimezone?: string; + /** * The duration after a backup is scheduled before a job is canceled if it doesn't start successfully. * @@ -213,6 +224,10 @@ export class BackupPlanRule { throw new Error('`scheduleExpression` must be of type `cron`'); } + if (props.scheduleExpressionTimezone && !props.scheduleExpression) { + throw new Error('Cannot specify `scheduleExpressionTimezone` for a rule without a `scheduleExpression`'); + } + const deleteAfter = (props.enableContinuousBackup && !props.deleteAfter) ? Duration.days(35) : props.deleteAfter; if (props.enableContinuousBackup && props.moveToColdStorageAfter) { diff --git a/packages/aws-cdk-lib/aws-backup/test/plan.test.ts b/packages/aws-cdk-lib/aws-backup/test/plan.test.ts index 4d9c23700d651..ddbe3b0e3b535 100644 --- a/packages/aws-cdk-lib/aws-backup/test/plan.test.ts +++ b/packages/aws-cdk-lib/aws-backup/test/plan.test.ts @@ -373,6 +373,41 @@ test('create a plan and add rule with recoveryPointTags', () => { }); }); +test('specify timezone for schedule expression', () => { + // WHEN + new BackupPlan(stack, 'Plan', { + backupPlanRules: [ + new BackupPlanRule({ + scheduleExpression: events.Schedule.cron({ + hour: '6', + minute: '0', + }), + scheduleExpressionTimezone: 'America/Los_Angeles', + }), + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Backup::BackupPlan', { + BackupPlan: { + BackupPlanName: 'Plan', + BackupPlanRule: [ + { + RuleName: 'PlanRule0', + TargetBackupVault: { + 'Fn::GetAtt': [ + 'PlanVault0284B0C2', + 'BackupVaultName', + ], + }, + ScheduleExpression: 'cron(0 6 * * ? *)', + ScheduleExpressionTimezone: 'America/Los_Angeles', + }, + ], + }, + }); +}); + test('throws when deleteAfter is not greater than moveToColdStorageAfter', () => { expect(() => new BackupPlanRule({ deleteAfter: Duration.days(5),