From 71edd36d1dce9ecfd0f349e5d0304ed5e9e3a4f6 Mon Sep 17 00:00:00 2001 From: Andrew Hammond <445764+ahammond@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:30:47 -0800 Subject: [PATCH 1/2] feat: enable Retain [INFRA-60445] --- src/aurora.ts | 33 +++++++++++++++++-- test/aurora.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/aurora.ts b/src/aurora.ts index 269880c..cc883b2 100644 --- a/src/aurora.ts +++ b/src/aurora.ts @@ -1,6 +1,7 @@ import { join } from 'path'; import { Annotations, + Aspects, aws_ec2, aws_iam, aws_kms, @@ -8,15 +9,17 @@ import { aws_lambda_nodejs, aws_logs, aws_rds, + CfnResource, // CfnMapping, CfnOutput, custom_resources, CustomResource, Duration, + IAspect, RemovalPolicy, Stack, } from 'aws-cdk-lib'; -import { Construct, IDependable } from 'constructs'; +import { Construct, IDependable, IConstruct } from 'constructs'; import { Namer } from 'multi-convention-namer'; import {} from './aurora.provision-database'; @@ -31,6 +34,20 @@ declare global { interface ReadableStream {} } +/** + * Aspect that applies a RemovalPolicy to all CloudFormation resources in a construct tree. + * This ensures consistent deletion behavior across all resources when the stack is deleted. + */ +class ApplyRemovalPolicyAspect implements IAspect { + constructor(private readonly policy: RemovalPolicy) {} + + visit(node: IConstruct): void { + if (node instanceof CfnResource) { + node.applyRemovalPolicy(this.policy); + } + } +} + export interface AuroraProps { /** * Turn on the Activity Stream feature of the Aurora cluster. @@ -96,7 +113,14 @@ export interface AuroraProps { */ readonly proxySecurityGroups?: aws_ec2.ISecurityGroup[]; /** - * @default - passthrough + * The removal policy to apply to all resources in this construct. + * + * Controls what happens to resources when the CloudFormation stack is deleted: + * - RemovalPolicy.RETAIN: Resources are orphaned (recommended for production databases) + * - RemovalPolicy.DESTROY: Resources are deleted with the stack + * - RemovalPolicy.SNAPSHOT: Resources are snapshotted before deletion (where supported) + * + * @default RemovalPolicy.RETAIN - All resources are retained for safety */ readonly removalPolicy?: RemovalPolicy; /** @@ -380,7 +404,6 @@ export class Aurora extends Construct { ), parameterGroup: props.parameterGroup, parameters: parameters, - removalPolicy: props.removalPolicy, storageEncryptionKey: encryptionKey, }); @@ -603,5 +626,9 @@ export class Aurora extends Construct { rdsUser.node.addDependency(this.cluster); }); } + + // Apply removal policy to all resources (defaults to RETAIN for safety) + const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN; + Aspects.of(this).add(new ApplyRemovalPolicyAspect(removalPolicy)); } } diff --git a/test/aurora.test.ts b/test/aurora.test.ts index 67f3edb..8889781 100644 --- a/test/aurora.test.ts +++ b/test/aurora.test.ts @@ -143,10 +143,29 @@ describe('Aurora', () => { it('proxyName', () => { template.hasResourceProperties('AWS::RDS::DBProxy', { DBProxyName: 'Test' }); }); - it('removalPolicy', () => { - template.hasResource('AWS::RDS::DBCluster', { - UpdateReplacePolicy: 'Snapshot', - DeletionPolicy: 'Snapshot', + it('removalPolicy defaults to RETAIN for all resources', () => { + // Get all resources from the template + const templateJson = template.toJSON(); + const resources = templateJson.Resources || {}; + + // Check specific resource types that support DeletionPolicy + const supportedTypes = [ + 'AWS::RDS::DBCluster', + 'AWS::RDS::DBInstance', + 'AWS::SecretsManager::Secret', + 'AWS::RDS::DBProxy', + ]; + + const resourcesWithPolicy = Object.entries(resources).filter(([_, resource]: [string, any]) => + supportedTypes.includes(resource.Type), + ); + + // Verify we found some resources + expect(resourcesWithPolicy.length).toBeGreaterThan(0); + + // Check that these resources have DeletionPolicy: Retain by default + resourcesWithPolicy.forEach(([, resource]: [string, any]) => { + expect(resource.DeletionPolicy).toBe('Retain'); }); }); it('retention', () => { @@ -428,5 +447,55 @@ describe('Aurora', () => { Parameters: { test: 'rds' }, }); }); + + describe('removalPolicy aspect', () => { + test.each([ + { + removalPolicy: RemovalPolicy.RETAIN, + expectedPolicy: 'Retain', + description: 'applies RETAIN policy to all resources', + }, + { + removalPolicy: RemovalPolicy.DESTROY, + expectedPolicy: 'Delete', + description: 'applies DESTROY policy to all resources', + }, + { + removalPolicy: RemovalPolicy.SNAPSHOT, + expectedPolicy: 'Snapshot', + description: 'applies SNAPSHOT policy to snapshot-capable resources', + }, + ])('$description when explicitly set to $removalPolicy', ({ removalPolicy, expectedPolicy }) => { + createAurora({ ...defaultAuroraProps, removalPolicy }); + + const templateJson = template.toJSON(); + const resources = templateJson.Resources || {}; + + // Check specific resource types that support DeletionPolicy + const supportedTypes = [ + 'AWS::RDS::DBCluster', + 'AWS::RDS::DBInstance', + 'AWS::SecretsManager::Secret', + 'AWS::RDS::DBProxy', + ]; + + const resourcesWithPolicy = Object.entries(resources).filter(([_, resource]: [string, any]) => + supportedTypes.includes(resource.Type), + ); + + // Verify we found some resources + expect(resourcesWithPolicy.length).toBeGreaterThan(0); + + // Check that these resources have the expected DeletionPolicy + resourcesWithPolicy.forEach(([, resource]: [string, any]) => { + if (removalPolicy === RemovalPolicy.DESTROY) { + // DESTROY maps to CloudFormation's Delete (or undefined which defaults to Delete) + expect(['Delete', undefined]).toContain(resource.DeletionPolicy); + } else { + expect(resource.DeletionPolicy).toBe(expectedPolicy); + } + }); + }); + }); }); }); From 78cb64cdce5f1a2eda200c2780d125064774c957 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 23:05:08 +0000 Subject: [PATCH 2/2] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- API.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index 67a1153..73d1fd4 100644 --- a/API.md +++ b/API.md @@ -249,7 +249,7 @@ const auroraProps: AuroraProps = { ... } | performanceInsightRetention | aws-cdk-lib.aws_rds.PerformanceInsightRetention | How long to retain performance insights data in days. | | postgresEngineVersion | aws-cdk-lib.aws_rds.AuroraPostgresEngineVersion | Postgres version Be aware of version limitations See https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraFeaturesRegionsDBEngines.grids.html#Concepts.Aurora_Fea_Regions_DB-eng.Feature.RDS_Proxy. | | proxySecurityGroups | aws-cdk-lib.aws_ec2.ISecurityGroup[] | Security groups to use for the RDS Proxy. | -| removalPolicy | aws-cdk-lib.RemovalPolicy | *No description.* | +| removalPolicy | aws-cdk-lib.RemovalPolicy | The removal policy to apply to all resources in this construct. | | retention | aws-cdk-lib.Duration | RDS backup retention. | | schemas | string[] | Schemas to create and grant defaults for users. | | secretPrefix | string \| multi-convention-namer.Namer | Prefix for secrets. | @@ -495,7 +495,14 @@ public readonly removalPolicy: RemovalPolicy; ``` - *Type:* aws-cdk-lib.RemovalPolicy -- *Default:* passthrough +- *Default:* RemovalPolicy.RETAIN - All resources are retained for safety + +The removal policy to apply to all resources in this construct. + +Controls what happens to resources when the CloudFormation stack is deleted: +- RemovalPolicy.RETAIN: Resources are orphaned (recommended for production databases) +- RemovalPolicy.DESTROY: Resources are deleted with the stack +- RemovalPolicy.SNAPSHOT: Resources are snapshotted before deletion (where supported) ---