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)
---
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);
+ }
+ });
+ });
+ });
});
});