Skip to content

Commit

Permalink
feat(backup): add copy actions to backup plan rules (aws#22244)
Browse files Browse the repository at this point in the history
This PR adds the ability to specify copy actions on backup plan rules, to copy recovery points to a different vault. 

```ts
declare const plan: backup.BackupPlan;
declare const secondaryVault: backup.BackupVault
plan.addRule(new backup.BackupPlanRule({
  copyActions: [{
    destinationBackupVault: secondaryVault,
    moveToColdStorageAfter: Duration.days(30),
    deleteAfter: Duration.days(120),
  }]
}));
```

The naming and types for `moveToColdStorageAfter` and `deleteAfter` are consistent with the parent `Rule` type to avoid confusion.

This closes aws#22173.

Note: While working on this, I discovered that there's a gap in the validation for `Rule` where if both `moveToColdStorageAfter` and `deleteAfter` are specified, then `deleteAfter` must be at least 90 days later than the `moveToColdStorageAfter` point. I've added validation for this in the `copyActions` validation, but I think fixing this for the rest of `Rule` probably warrants a separate bug fix issue and PR.

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rdbatch authored Sep 27, 2022
1 parent fcb311d commit d87a651
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 16 deletions.
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-backup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ plan.addRule(new backup.BackupPlanRule({
}));
```

Rules can also specify to copy recovery points to another Backup Vault using `copyActions`. Copied recovery points can
optionally have `moveToColdStorageAfter` and `deleteAfter` configured.

```ts
declare const plan: backup.BackupPlan;
declare const secondaryVault: backup.BackupVault;
plan.addRule(new backup.BackupPlanRule({
copyActions: [{
destinationBackupVault: secondaryVault,
moveToColdStorageAfter: Duration.days(30),
deleteAfter: Duration.days(120),
}]
}));
```

Ready-made rules are also available:

```ts
Expand Down
13 changes: 12 additions & 1 deletion packages/@aws-cdk/aws-backup/lib/plan.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IResource, Lazy, Resource } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnBackupPlan } from './backup.generated';
import { BackupPlanRule } from './rule';
import { BackupPlanCopyActionProps, BackupPlanRule } from './rule';
import { BackupSelection, BackupSelectionOptions } from './selection';
import { BackupVault, IBackupVault } from './vault';

Expand Down Expand Up @@ -191,9 +191,20 @@ export class BackupPlan extends Resource implements IBackupPlan {
startWindowMinutes: rule.props.startWindow?.toMinutes(),
enableContinuousBackup: rule.props.enableContinuousBackup,
targetBackupVault: vault.backupVaultName,
copyActions: rule.props.copyActions?.map(this.planCopyActions),
});
}

private planCopyActions(props: BackupPlanCopyActionProps): CfnBackupPlan.CopyActionResourceTypeProperty {
return {
destinationBackupVaultArn: props.destinationBackupVault.backupVaultArn,
lifecycle: (props.deleteAfter || props.moveToColdStorageAfter) && {
deleteAfterDays: props.deleteAfter?.toDays(),
moveToColdStorageAfterDays: props.moveToColdStorageAfter?.toDays(),
},
};
}

/**
* The backup vault where backups are stored if not defined at
* the rule level
Expand Down
47 changes: 46 additions & 1 deletion packages/@aws-cdk/aws-backup/lib/rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as events from '@aws-cdk/aws-events';
import { Duration } from '@aws-cdk/core';
import { Duration, Token } from '@aws-cdk/core';
import { IBackupVault } from './vault';

/**
Expand Down Expand Up @@ -70,6 +70,38 @@ export interface BackupPlanRuleProps {
* @default false
*/
readonly enableContinuousBackup?: boolean;

/**
* Copy operations to perform on recovery points created by this rule
*
* @default - no copy actions
*/
readonly copyActions?: BackupPlanCopyActionProps[];
}

/**
* Properties for a BackupPlanCopyAction
*/
export interface BackupPlanCopyActionProps {
/**
* Destination Vault for recovery points to be copied into
*/
readonly destinationBackupVault: IBackupVault;

/**
* Specifies the duration after creation that a copied recovery point is deleted from the destination vault.
* Must be at least 90 days greater than `moveToColdStorageAfter`, if specified.
*
* @default - recovery point is never deleted
*/
readonly deleteAfter?: Duration;

/**
* Specifies the duration after creation that a copied recovery point is moved to cold storage.
*
* @default - recovery point is never moved to cold storage
*/
readonly moveToColdStorageAfter?: Duration;
}

/**
Expand Down Expand Up @@ -185,6 +217,19 @@ export class BackupPlanRule {
throw new Error(`'deleteAfter' must be between 1 and 35 days if 'enableContinuousBackup' is enabled, but got ${props.deleteAfter.toHumanString()}`);
}

if (props.copyActions && props.copyActions.length > 0) {
props.copyActions.forEach(copyAction => {
if (copyAction.deleteAfter && !Token.isUnresolved(copyAction.deleteAfter) &&
copyAction.moveToColdStorageAfter && !Token.isUnresolved(copyAction.moveToColdStorageAfter) &&
copyAction.deleteAfter.toDays() < copyAction.moveToColdStorageAfter.toDays() + 90) {
throw new Error([
'\'deleteAfter\' must at least 90 days later than corresponding \'moveToColdStorageAfter\'',
`received 'deleteAfter: ${copyAction.deleteAfter.toDays()}' and 'moveToColdStorageAfter: ${copyAction.moveToColdStorageAfter.toDays()}'`,
].join('\n'));
}
});
}

this.props = {
...props,
deleteAfter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "20.0.0",
"version": "21.0.0",
"files": {
"8001d34381bcb57b7b2a8fb3ade1e27b0ea7c1819c1d3973537e2cb5aa604ce7": {
"14e034eeffbdd95a18b6c1a8c7a4876e1dfbedde51220bb1a196a337a6848c16": {
"source": {
"path": "cdk-backup.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "8001d34381bcb57b7b2a8fb3ade1e27b0ea7c1819c1d3973537e2cb5aa604ce7.json",
"objectKey": "14e034eeffbdd95a18b6c1a8c7a4876e1dfbedde51220bb1a196a337a6848c16.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"SecondaryVault67665B5E": {
"Type": "AWS::Backup::BackupVault",
"Properties": {
"BackupVaultName": "cdkbackupSecondaryVaultA01C2A0E",
"LockConfiguration": {
"MinRetentionDays": 5
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"PlanDAF4E53A": {
"Type": "AWS::Backup::BackupPlan",
"Properties": {
Expand Down Expand Up @@ -84,6 +95,29 @@
"BackupVaultName"
]
}
},
{
"CopyActions": [
{
"DestinationBackupVaultArn": {
"Fn::GetAtt": [
"SecondaryVault67665B5E",
"BackupVaultArn"
]
},
"Lifecycle": {
"DeleteAfterDays": 120,
"MoveToColdStorageAfterDays": 30
}
}
],
"RuleName": "PlanRule3",
"TargetBackupVault": {
"Fn::GetAtt": [
"Vault23237E5B",
"BackupVaultName"
]
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"21.0.0"}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "20.0.0",
"version": "21.0.0",
"testCases": {
"integ.backup": {
"stacks": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "20.0.0",
"version": "21.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand All @@ -23,7 +23,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}/8001d34381bcb57b7b2a8fb3ade1e27b0ea7c1819c1d3973537e2cb5aa604ce7.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/14e034eeffbdd95a18b6c1a8c7a4876e1dfbedde51220bb1a196a337a6848c16.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
Expand Down Expand Up @@ -57,6 +57,12 @@
"data": "Vault23237E5B"
}
],
"/cdk-backup/SecondaryVault/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "SecondaryVault67665B5E"
}
],
"/cdk-backup/Plan/Resource": [
{
"type": "aws:cdk:logicalId",
Expand Down
64 changes: 57 additions & 7 deletions packages/@aws-cdk/aws-backup/test/backup.integ.snapshot/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"path": "Tree",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"version": "10.1.102"
}
},
"cdk-backup": {
Expand Down Expand Up @@ -53,8 +53,8 @@
"id": "ScalingRole",
"path": "cdk-backup/Table/ScalingRole",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"fqn": "@aws-cdk/core.Resource",
"version": "0.0.0"
}
}
},
Expand Down Expand Up @@ -102,6 +102,33 @@
"version": "0.0.0"
}
},
"SecondaryVault": {
"id": "SecondaryVault",
"path": "cdk-backup/SecondaryVault",
"children": {
"Resource": {
"id": "Resource",
"path": "cdk-backup/SecondaryVault/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::Backup::BackupVault",
"aws:cdk:cloudformation:props": {
"backupVaultName": "cdkbackupSecondaryVaultA01C2A0E",
"lockConfiguration": {
"minRetentionDays": 5
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-backup.CfnBackupVault",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-backup.BackupVault",
"version": "0.0.0"
}
},
"Plan": {
"id": "Plan",
"path": "cdk-backup/Plan",
Expand Down Expand Up @@ -154,6 +181,29 @@
"BackupVaultName"
]
}
},
{
"ruleName": "PlanRule3",
"targetBackupVault": {
"Fn::GetAtt": [
"Vault23237E5B",
"BackupVaultName"
]
},
"copyActions": [
{
"destinationBackupVaultArn": {
"Fn::GetAtt": [
"SecondaryVault67665B5E",
"BackupVaultArn"
]
},
"lifecycle": {
"deleteAfterDays": 120,
"moveToColdStorageAfterDays": 30
}
}
]
}
]
}
Expand Down Expand Up @@ -314,14 +364,14 @@
}
},
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"fqn": "@aws-cdk/core.Stack",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"fqn": "@aws-cdk/core.App",
"version": "0.0.0"
}
}
}
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-backup/test/integ.backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class TestStack extends Stack {
minRetention: Duration.days(5),
},
});
const secondaryVault = new backup.BackupVault(this, 'SecondaryVault', {
removalPolicy: RemovalPolicy.DESTROY,
lockConfiguration: {
minRetention: Duration.days(5),
},
});
const plan = backup.BackupPlan.dailyWeeklyMonthly5YearRetention(this, 'Plan', vault);

plan.addSelection('Selection', {
Expand All @@ -33,6 +39,14 @@ class TestStack extends Stack {
backup.BackupResource.fromTag('stage', 'prod'), // Resources that are tagged stage=prod
],
});

plan.addRule(new backup.BackupPlanRule({
copyActions: [{
destinationBackupVault: secondaryVault,
moveToColdStorageAfter: Duration.days(30),
deleteAfter: Duration.days(120),
}],
}));
}
}

Expand Down
Loading

0 comments on commit d87a651

Please sign in to comment.