Skip to content

Commit e1efb33

Browse files
feat: enable Retain [INFRA-60445] (#129)
* feat: enable Retain [INFRA-60445] * chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent dbe1df6 commit e1efb33

File tree

3 files changed

+112
-9
lines changed

3 files changed

+112
-9
lines changed

API.md

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/aurora.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { join } from 'path';
22
import {
33
Annotations,
4+
Aspects,
45
aws_ec2,
56
aws_iam,
67
aws_kms,
78
aws_lambda,
89
aws_lambda_nodejs,
910
aws_logs,
1011
aws_rds,
12+
CfnResource,
1113
// CfnMapping,
1214
CfnOutput,
1315
custom_resources,
1416
CustomResource,
1517
Duration,
18+
IAspect,
1619
RemovalPolicy,
1720
Stack,
1821
} from 'aws-cdk-lib';
19-
import { Construct, IDependable } from 'constructs';
22+
import { Construct, IDependable, IConstruct } from 'constructs';
2023
import { Namer } from 'multi-convention-namer';
2124

2225
import {} from './aurora.provision-database';
@@ -31,6 +34,20 @@ declare global {
3134
interface ReadableStream {}
3235
}
3336

37+
/**
38+
* Aspect that applies a RemovalPolicy to all CloudFormation resources in a construct tree.
39+
* This ensures consistent deletion behavior across all resources when the stack is deleted.
40+
*/
41+
class ApplyRemovalPolicyAspect implements IAspect {
42+
constructor(private readonly policy: RemovalPolicy) {}
43+
44+
visit(node: IConstruct): void {
45+
if (node instanceof CfnResource) {
46+
node.applyRemovalPolicy(this.policy);
47+
}
48+
}
49+
}
50+
3451
export interface AuroraProps {
3552
/**
3653
* Turn on the Activity Stream feature of the Aurora cluster.
@@ -96,7 +113,14 @@ export interface AuroraProps {
96113
*/
97114
readonly proxySecurityGroups?: aws_ec2.ISecurityGroup[];
98115
/**
99-
* @default - passthrough
116+
* The removal policy to apply to all resources in this construct.
117+
*
118+
* Controls what happens to resources when the CloudFormation stack is deleted:
119+
* - RemovalPolicy.RETAIN: Resources are orphaned (recommended for production databases)
120+
* - RemovalPolicy.DESTROY: Resources are deleted with the stack
121+
* - RemovalPolicy.SNAPSHOT: Resources are snapshotted before deletion (where supported)
122+
*
123+
* @default RemovalPolicy.RETAIN - All resources are retained for safety
100124
*/
101125
readonly removalPolicy?: RemovalPolicy;
102126
/**
@@ -380,7 +404,6 @@ export class Aurora extends Construct {
380404
),
381405
parameterGroup: props.parameterGroup,
382406
parameters: parameters,
383-
removalPolicy: props.removalPolicy,
384407
storageEncryptionKey: encryptionKey,
385408
});
386409

@@ -603,5 +626,9 @@ export class Aurora extends Construct {
603626
rdsUser.node.addDependency(this.cluster);
604627
});
605628
}
629+
630+
// Apply removal policy to all resources (defaults to RETAIN for safety)
631+
const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN;
632+
Aspects.of(this).add(new ApplyRemovalPolicyAspect(removalPolicy));
606633
}
607634
}

test/aurora.test.ts

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,29 @@ describe('Aurora', () => {
143143
it('proxyName', () => {
144144
template.hasResourceProperties('AWS::RDS::DBProxy', { DBProxyName: 'Test' });
145145
});
146-
it('removalPolicy', () => {
147-
template.hasResource('AWS::RDS::DBCluster', {
148-
UpdateReplacePolicy: 'Snapshot',
149-
DeletionPolicy: 'Snapshot',
146+
it('removalPolicy defaults to RETAIN for all resources', () => {
147+
// Get all resources from the template
148+
const templateJson = template.toJSON();
149+
const resources = templateJson.Resources || {};
150+
151+
// Check specific resource types that support DeletionPolicy
152+
const supportedTypes = [
153+
'AWS::RDS::DBCluster',
154+
'AWS::RDS::DBInstance',
155+
'AWS::SecretsManager::Secret',
156+
'AWS::RDS::DBProxy',
157+
];
158+
159+
const resourcesWithPolicy = Object.entries(resources).filter(([_, resource]: [string, any]) =>
160+
supportedTypes.includes(resource.Type),
161+
);
162+
163+
// Verify we found some resources
164+
expect(resourcesWithPolicy.length).toBeGreaterThan(0);
165+
166+
// Check that these resources have DeletionPolicy: Retain by default
167+
resourcesWithPolicy.forEach(([, resource]: [string, any]) => {
168+
expect(resource.DeletionPolicy).toBe('Retain');
150169
});
151170
});
152171
it('retention', () => {
@@ -428,5 +447,55 @@ describe('Aurora', () => {
428447
Parameters: { test: 'rds' },
429448
});
430449
});
450+
451+
describe('removalPolicy aspect', () => {
452+
test.each([
453+
{
454+
removalPolicy: RemovalPolicy.RETAIN,
455+
expectedPolicy: 'Retain',
456+
description: 'applies RETAIN policy to all resources',
457+
},
458+
{
459+
removalPolicy: RemovalPolicy.DESTROY,
460+
expectedPolicy: 'Delete',
461+
description: 'applies DESTROY policy to all resources',
462+
},
463+
{
464+
removalPolicy: RemovalPolicy.SNAPSHOT,
465+
expectedPolicy: 'Snapshot',
466+
description: 'applies SNAPSHOT policy to snapshot-capable resources',
467+
},
468+
])('$description when explicitly set to $removalPolicy', ({ removalPolicy, expectedPolicy }) => {
469+
createAurora({ ...defaultAuroraProps, removalPolicy });
470+
471+
const templateJson = template.toJSON();
472+
const resources = templateJson.Resources || {};
473+
474+
// Check specific resource types that support DeletionPolicy
475+
const supportedTypes = [
476+
'AWS::RDS::DBCluster',
477+
'AWS::RDS::DBInstance',
478+
'AWS::SecretsManager::Secret',
479+
'AWS::RDS::DBProxy',
480+
];
481+
482+
const resourcesWithPolicy = Object.entries(resources).filter(([_, resource]: [string, any]) =>
483+
supportedTypes.includes(resource.Type),
484+
);
485+
486+
// Verify we found some resources
487+
expect(resourcesWithPolicy.length).toBeGreaterThan(0);
488+
489+
// Check that these resources have the expected DeletionPolicy
490+
resourcesWithPolicy.forEach(([, resource]: [string, any]) => {
491+
if (removalPolicy === RemovalPolicy.DESTROY) {
492+
// DESTROY maps to CloudFormation's Delete (or undefined which defaults to Delete)
493+
expect(['Delete', undefined]).toContain(resource.DeletionPolicy);
494+
} else {
495+
expect(resource.DeletionPolicy).toBe(expectedPolicy);
496+
}
497+
});
498+
});
499+
});
431500
});
432501
});

0 commit comments

Comments
 (0)