Skip to content

Commit 5a32b1a

Browse files
committed
feat(rds): change the default retention policy of Cluster and DB Instance to Snapshot
The 'Snapshot' retention policy is a special one used only for RDS. It deletes the underlying resource, but before doing that, creates a snapshot of it, so that the data is not lost. Use the 'Snapshot' policy instead of 'Retain', for the DatabaseCluster and DbInstance resources. Fixes aws#3298 BREAKING CHANGE: the default retention policy for RDS Cluster and DbInstance is now 'Snapshot'
1 parent 60814b7 commit 5a32b1a

11 files changed

+76
-63
lines changed

packages/@aws-cdk/aws-rds/lib/cluster.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
33
import * as kms from '@aws-cdk/aws-kms';
44
import * as s3 from '@aws-cdk/aws-s3';
55
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
6-
import { Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core';
6+
import { CfnDeletionPolicy, Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core';
77
import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref';
88
import { DatabaseSecret } from './database-secret';
99
import { Endpoint } from './endpoint';
@@ -124,9 +124,9 @@ export interface DatabaseClusterProps {
124124
* The removal policy to apply when the cluster and its instances are removed
125125
* from the stack or replaced during an update.
126126
*
127-
* @default - Retain cluster.
127+
* @default - Snapshot (remove the cluster and instances, but retain a snapshot of the data)
128128
*/
129-
readonly removalPolicy?: RemovalPolicy
129+
readonly removalPolicy?: RemovalPolicy;
130130

131131
/**
132132
* The interval, in seconds, between points when Amazon RDS collects enhanced
@@ -461,9 +461,16 @@ export class DatabaseCluster extends DatabaseClusterBase {
461461
storageEncrypted: props.kmsKey ? true : props.storageEncrypted,
462462
});
463463

464-
cluster.applyRemovalPolicy(props.removalPolicy, {
465-
applyToUpdateReplacePolicy: true,
466-
});
464+
// if removalPolicy was not specified,
465+
// leave it as the default, which is Snapshot
466+
if (props.removalPolicy) {
467+
cluster.applyRemovalPolicy(props.removalPolicy);
468+
} else {
469+
// The CFN default makes sense for DeletionPolicy,
470+
// but doesn't cover UpdateReplacePolicy.
471+
// Fix that here.
472+
cluster.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT;
473+
}
467474

468475
this.clusterIdentifier = cluster.ref;
469476

@@ -519,9 +526,13 @@ export class DatabaseCluster extends DatabaseClusterBase {
519526
monitoringRoleArn: monitoringRole && monitoringRole.roleArn,
520527
});
521528

522-
instance.applyRemovalPolicy(props.removalPolicy, {
523-
applyToUpdateReplacePolicy: true,
524-
});
529+
// If removalPolicy isn't explicitly set,
530+
// it's Snapshot for Cluster.
531+
// Because of that, in this case,
532+
// we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set.
533+
if (props.removalPolicy) {
534+
instance.applyRemovalPolicy(props.removalPolicy);
535+
}
525536

526537
// We must have a dependency on the NAT gateway provider here to create
527538
// things in the right order.

packages/@aws-cdk/aws-rds/lib/instance.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DatabaseSecret } from './database-secret';
1010
import { Endpoint } from './endpoint';
1111
import { IOptionGroup } from './option-group';
1212
import { IParameterGroup } from './parameter-group';
13+
import { applyInstanceDeletionPolicy } from './private/instance-deletion-policy';
1314
import { DatabaseClusterEngine, RotationMultiUserOptions } from './props';
1415
import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated';
1516

@@ -536,7 +537,7 @@ export interface DatabaseInstanceNewProps {
536537
* The CloudFormation policy to apply when the instance is removed from the
537538
* stack or replaced during an update.
538539
*
539-
* @default RemovalPolicy.Retain
540+
* @default Snapshot (remove the resource, but retain a snapshot of the data)
540541
*/
541542
readonly removalPolicy?: RemovalPolicy
542543

@@ -886,9 +887,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas
886887
const portAttribute = Token.asNumber(instance.attrEndpointPort);
887888
this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute);
888889

889-
instance.applyRemovalPolicy(props.removalPolicy, {
890-
applyToUpdateReplacePolicy: true,
891-
});
890+
applyInstanceDeletionPolicy(instance, props.removalPolicy);
892891

893892
if (secret) {
894893
this.secret = secret.attach(this);
@@ -984,9 +983,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
984983
const portAttribute = Token.asNumber(instance.attrEndpointPort);
985984
this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute);
986985

987-
instance.applyRemovalPolicy(props.removalPolicy, {
988-
applyToUpdateReplacePolicy: true,
989-
});
986+
applyInstanceDeletionPolicy(instance, props.removalPolicy);
990987

991988
if (secret) {
992989
this.secret = secret.attach(this);
@@ -1054,9 +1051,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements
10541051
const portAttribute = Token.asNumber(instance.attrEndpointPort);
10551052
this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute);
10561053

1057-
instance.applyRemovalPolicy(props.removalPolicy, {
1058-
applyToUpdateReplacePolicy: true,
1059-
});
1054+
applyInstanceDeletionPolicy(instance, props.removalPolicy);
10601055

10611056
this.setLogRetention();
10621057
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { CfnDeletionPolicy, RemovalPolicy } from '@aws-cdk/core';
2+
import { CfnDBInstance } from '../rds.generated';
3+
4+
export function applyInstanceDeletionPolicy(cfnDbInstance: CfnDBInstance, removalPolicy: RemovalPolicy | undefined): void {
5+
if (!removalPolicy) {
6+
// the default DeletionPolicy is 'Snapshot', which is fine,
7+
// but we should also make it 'Snapshot' for UpdateReplace policy
8+
cfnDbInstance.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT;
9+
} else {
10+
// just apply whatever removal policy the customer explicitly provided
11+
cfnDbInstance.applyRemovalPolicy(removalPolicy);
12+
}
13+
}

packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json

+4-9
Original file line numberDiff line numberDiff line change
@@ -706,8 +706,7 @@
706706
}
707707
]
708708
},
709-
"UpdateReplacePolicy": "Retain",
710-
"DeletionPolicy": "Retain"
709+
"UpdateReplacePolicy": "Snapshot"
711710
},
712711
"DatabaseInstance1844F58FD": {
713712
"Type": "AWS::RDS::DBInstance",
@@ -725,9 +724,7 @@
725724
"VPCPrivateSubnet1DefaultRouteAE1D6490",
726725
"VPCPrivateSubnet2DefaultRouteF4F5CFD2",
727726
"VPCPrivateSubnet3DefaultRoute27F311AE"
728-
],
729-
"UpdateReplacePolicy": "Retain",
730-
"DeletionPolicy": "Retain"
727+
]
731728
},
732729
"DatabaseInstance2AA380DEE": {
733730
"Type": "AWS::RDS::DBInstance",
@@ -745,9 +742,7 @@
745742
"VPCPrivateSubnet1DefaultRouteAE1D6490",
746743
"VPCPrivateSubnet2DefaultRouteF4F5CFD2",
747744
"VPCPrivateSubnet3DefaultRoute27F311AE"
748-
],
749-
"UpdateReplacePolicy": "Retain",
750-
"DeletionPolicy": "Retain"
745+
]
751746
},
752747
"DatabaseRotationSingleUserSecurityGroupAC6E0E73": {
753748
"Type": "AWS::EC2::SecurityGroup",
@@ -817,4 +812,4 @@
817812
}
818813
}
819814
}
820-
}
815+
}

packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json

+4-9
Original file line numberDiff line numberDiff line change
@@ -668,8 +668,7 @@
668668
}
669669
]
670670
},
671-
"UpdateReplacePolicy": "Retain",
672-
"DeletionPolicy": "Retain"
671+
"UpdateReplacePolicy": "Snapshot"
673672
},
674673
"DatabaseInstance1844F58FD": {
675674
"Type": "AWS::RDS::DBInstance",
@@ -687,9 +686,7 @@
687686
"DependsOn": [
688687
"VPCPublicSubnet1DefaultRoute91CEF279",
689688
"VPCPublicSubnet2DefaultRouteB7481BBA"
690-
],
691-
"UpdateReplacePolicy": "Retain",
692-
"DeletionPolicy": "Retain"
689+
]
693690
},
694691
"DatabaseInstance2AA380DEE": {
695692
"Type": "AWS::RDS::DBInstance",
@@ -707,9 +704,7 @@
707704
"DependsOn": [
708705
"VPCPublicSubnet1DefaultRoute91CEF279",
709706
"VPCPublicSubnet2DefaultRouteB7481BBA"
710-
],
711-
"UpdateReplacePolicy": "Retain",
712-
"DeletionPolicy": "Retain"
707+
]
713708
}
714709
}
715-
}
710+
}

packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json

+4-9
Original file line numberDiff line numberDiff line change
@@ -500,8 +500,7 @@
500500
}
501501
]
502502
},
503-
"UpdateReplacePolicy": "Retain",
504-
"DeletionPolicy": "Retain"
503+
"UpdateReplacePolicy": "Snapshot"
505504
},
506505
"DatabaseInstance1844F58FD": {
507506
"Type": "AWS::RDS::DBInstance",
@@ -519,9 +518,7 @@
519518
"DependsOn": [
520519
"VPCPublicSubnet1DefaultRoute91CEF279",
521520
"VPCPublicSubnet2DefaultRouteB7481BBA"
522-
],
523-
"UpdateReplacePolicy": "Retain",
524-
"DeletionPolicy": "Retain"
521+
]
525522
},
526523
"DatabaseInstance2AA380DEE": {
527524
"Type": "AWS::RDS::DBInstance",
@@ -539,9 +536,7 @@
539536
"DependsOn": [
540537
"VPCPublicSubnet1DefaultRoute91CEF279",
541538
"VPCPublicSubnet2DefaultRouteB7481BBA"
542-
],
543-
"UpdateReplacePolicy": "Retain",
544-
"DeletionPolicy": "Retain"
539+
]
545540
}
546541
}
547-
}
542+
}

packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,7 @@
694694
}
695695
]
696696
},
697-
"UpdateReplacePolicy": "Retain",
698-
"DeletionPolicy": "Retain"
697+
"UpdateReplacePolicy": "Snapshot"
699698
},
700699
"InstanceLogRetentiontrace487771C8": {
701700
"Type": "Custom::LogRetention",
@@ -1122,4 +1121,4 @@
11221121
"Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\""
11231122
}
11241123
}
1125-
}
1124+
}

packages/@aws-cdk/aws-rds/test/test.cluster.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert';
1+
import { ABSENT, countResources, expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert';
22
import * as ec2 from '@aws-cdk/aws-ec2';
33
import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
44
import * as kms from '@aws-cdk/aws-kms';
@@ -8,7 +8,7 @@ import { Test } from 'nodeunit';
88
import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, ParameterGroup } from '../lib';
99

1010
export = {
11-
'check that instantiation works'(test: Test) {
11+
'creating a Cluster also creates 2 DB Instances'(test: Test) {
1212
// GIVEN
1313
const stack = testStack();
1414
const vpc = new ec2.Vpc(stack, 'VPC');
@@ -35,17 +35,19 @@ export = {
3535
MasterUserPassword: 'tooshort',
3636
VpcSecurityGroupIds: [ {'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId']}],
3737
},
38-
DeletionPolicy: 'Retain',
39-
UpdateReplacePolicy: 'Retain',
38+
DeletionPolicy: ABSENT,
39+
UpdateReplacePolicy: 'Snapshot',
4040
}, ResourcePart.CompleteDefinition));
4141

42+
expect(stack).to(countResources('AWS::RDS::DBInstance', 2));
4243
expect(stack).to(haveResource('AWS::RDS::DBInstance', {
43-
DeletionPolicy: 'Retain',
44-
UpdateReplacePolicy: 'Retain',
44+
DeletionPolicy: ABSENT,
45+
UpdateReplacePolicy: ABSENT,
4546
}, ResourcePart.CompleteDefinition));
4647

4748
test.done();
4849
},
50+
4951
'can create a cluster with a single instance'(test: Test) {
5052
// GIVEN
5153
const stack = testStack();

packages/@aws-cdk/aws-rds/test/test.instance.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert';
1+
import { ABSENT, countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert';
22
import * as ec2 from '@aws-cdk/aws-ec2';
33
import * as targets from '@aws-cdk/aws-events-targets';
44
import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
@@ -105,13 +105,8 @@ export = {
105105
},
106106
],
107107
},
108-
DeletionPolicy: 'Retain',
109-
UpdateReplacePolicy: 'Retain',
110-
}, ResourcePart.CompleteDefinition));
111-
112-
expect(stack).to(haveResource('AWS::RDS::DBInstance', {
113-
DeletionPolicy: 'Retain',
114-
UpdateReplacePolicy: 'Retain',
108+
DeletionPolicy: ABSENT,
109+
UpdateReplacePolicy: 'Snapshot',
115110
}, ResourcePart.CompleteDefinition));
116111

117112
expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', {

packages/@aws-cdk/core/lib/cfn-resource.ts

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export class CfnResource extends CfnRefElement {
116116
deletionPolicy = CfnDeletionPolicy.RETAIN;
117117
break;
118118

119+
case RemovalPolicy.SNAPSHOT:
120+
deletionPolicy = CfnDeletionPolicy.SNAPSHOT;
121+
break;
122+
119123
default:
120124
throw new Error(`Invalid removal policy: ${policy}`);
121125
}

packages/@aws-cdk/core/lib/removal-policy.ts

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ export enum RemovalPolicy {
1010
* in the account, but orphaned from the stack.
1111
*/
1212
RETAIN = 'retain',
13+
14+
/**
15+
* This retention policy deletes the resource,
16+
* but saves a snapshot of its data before deleting,
17+
* so that it can be re-created later.
18+
* Only available for some stateful resources,
19+
* like databases, EFS volumes, etc.
20+
*/
21+
SNAPSHOT = 'snapshot',
1322
}
1423

1524
export interface RemovalPolicyOptions {

0 commit comments

Comments
 (0)