diff --git a/API.md b/API.md
index 6322d6b..8981a9b 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 | Removal policy for infrastructure resources (RDS cluster, instances, secrets, security groups). |
| 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. |
@@ -488,19 +488,21 @@ Security groups to use for the RDS Proxy.
---
-##### ~~`removalPolicy`~~Optional
-
-- *Deprecated:* This property is no longer used. Removal policies are now automatically
-set based on resource type: infrastructure resources (KMS, RDS, Security Groups, Secrets)
-are RETAINED for safety, while ephemeral resources (Lambdas, IAM, nested stacks) are DESTROYED.
-This enables safe CDK->Terragrunt migrations.
+##### `removalPolicy`Optional
```typescript
public readonly removalPolicy: RemovalPolicy;
```
- *Type:* aws-cdk-lib.RemovalPolicy
-- *Default:* Automatic based on resource type
+- *Default:* RemovalPolicy.DESTROY
+
+Removal policy for infrastructure resources (RDS cluster, instances, secrets, security groups).
+
+Use `RemovalPolicy.RETAIN` for production databases to prevent accidental deletion.
+Use `RemovalPolicy.DESTROY` for development/QA environments to enable automatic cleanup.
+
+Ephemeral resources (lambdas, IAM roles, log groups) are always DESTROYED regardless of this setting.
---
diff --git a/CLAUDE.md b/CLAUDE.md
index 14c8ee5..fb0ae89 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,15 +6,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`@time-loop/cdk-aurora` is an opinionated AWS CDK construct library for deploying PostgreSQL Aurora clusters with best practices baked in. It provides a high-level abstraction that includes user management, secrets rotation, RDS Proxy, and database provisioning through custom CloudFormation resources.
+## Current Work: Configurable Removal Policy (optional-retain branch)
+
+The `optional-retain` branch contains work to implement the requirements in `AURORA-REMOVAL-POLICY-PRD.md`:
+- Making the `removalPolicy` prop functional again (currently deprecated and ignored)
+- Changing default from automatic RETAIN to DESTROY
+- Allowing users to explicitly configure RETAIN for production databases
+- This is a **breaking change** from the current hardcoded RETAIN behavior
+- Integration with downstream `@time-loop/cdk-library` which has already implemented support for this
+
## Key Architecture Components
### Main Construct (`src/aurora.ts`)
The `Aurora` class is the primary construct that orchestrates:
-- **Aurora PostgreSQL Cluster**: Configured with encryption, CloudWatch logs, and performance insights
+- **Aurora PostgreSQL Cluster**: Configured with encryption, CloudWatch logs, performance insights, and extensive default parameters optimized for production use
- **Three User Types**: Manager (admin), Writer (DML), and Reader (read-only) with separate SecretsManager secrets
- **RDS Proxy**: Optional proxy for connection pooling (default enabled, incompatible with multi-user rotation)
- **Custom Provisioners**: Lambda-backed CloudFormation custom resources for database and user provisioning
- **Activity Stream**: Optional database activity monitoring
+- **Automatic Removal Policies**: Infrastructure resources are retained, ephemeral resources are destroyed (via `ApplyRemovalPolicyAspect` at line 46-86)
### Provisioning System
Custom CloudFormation resources handle runtime database configuration:
@@ -98,10 +108,35 @@ The construct supports two mutually exclusive approaches for cluster configurati
**Default parameters** (used when neither is specified):
```typescript
{
+ // Logical replication
'rds.logical_replication': '1',
- max_replication_slots: '10',
- max_wal_senders: '10',
- wal_sender_timeout: '0'
+
+ // Replication configuration
+ max_replication_slots: '20',
+ max_wal_senders: '20',
+ max_logical_replication_workers: '8',
+
+ // Security
+ 'rds.force_ssl': '1',
+
+ // Performance monitoring and logging
+ log_min_duration_statement: '200',
+ log_lock_waits: '1',
+ log_statement: 'ddl',
+
+ // Query timeout protection
+ statement_timeout: '30000',
+ idle_in_transaction_session_timeout: '5000',
+
+ // Performance tuning
+ random_page_cost: '1.1',
+ work_mem: '4096',
+ max_parallel_workers_per_gather: '4',
+
+ // Autovacuum tuning
+ autovacuum_analyze_scale_factor: '0.01',
+
+ // ... and more (see src/aurora.ts:385-427 for complete list)
}
```
@@ -118,7 +153,16 @@ The construct supports two mutually exclusive approaches for cluster configurati
4. **Secret Naming**: Uses `multi-convention-namer` for consistent naming. Can prefix with `secretPrefix` for multiple clusters in same account.
-5. **Removal Policy**: The construct accepts `removalPolicy` prop but applies it differently across resources. Consider implementing the aspect pattern from `AURORA_DELETION_POLICY_IMPLEMENTATION.md` for comprehensive control.
+5. **Removal Policy** (⚠️ In Transition - see AURORA-REMOVAL-POLICY-PRD.md):
+ - **Current State**: The `removalPolicy` prop is deprecated and ignored. The `ApplyRemovalPolicyAspect` (src/aurora.ts:46-86) automatically applies:
+ - **RETAINED** (for safety): KMS keys, RDS clusters/instances, parameter groups, subnet groups, security groups (including ingress/egress rules), Secrets Manager secrets, and secret target attachments
+ - **DESTROYED** (ephemeral): Lambda functions, IAM roles/policies, CloudFormation nested stacks, resource policies, rotation schedules, SSM parameters, and CloudWatch log groups
+ - This was designed to enable safe CDK→Terragrunt migrations
+ - **Planned Change** (per PRD): Make `removalPolicy` configurable again:
+ - **Default**: `RemovalPolicy.DESTROY` for all infrastructure resources (breaking change from current RETAIN behavior)
+ - **Configurable**: Users can explicitly set `RETAIN` for production or `DESTROY` for dev/QA environments
+ - Remove deprecation warning
+ - Maintain automatic DESTROY for ephemeral resources (lambdas, IAM, etc.)
## Troubleshooting Database Issues
diff --git a/src/aurora.ts b/src/aurora.ts
index 01b8a8a..78655de 100644
--- a/src/aurora.ts
+++ b/src/aurora.ts
@@ -37,13 +37,12 @@ declare global {
/**
* Aspect that applies appropriate RemovalPolicies based on resource type.
*
- * Infrastructure resources (KMS, RDS, Security Groups, Secrets) are RETAINED for safety.
- * Ephemeral resources (Lambdas, IAM, nested stacks, secret attachments) are DESTROYED.
- *
- * This enables safe CDK->Terragrunt migrations where infrastructure is imported
- * but temporary CDK resources are cleaned up.
+ * Infrastructure resources (KMS, RDS, Security Groups, Secrets) use the configurable removal policy.
+ * Ephemeral resources (Lambdas, IAM, nested stacks) are always DESTROYED.
*/
class ApplyRemovalPolicyAspect implements IAspect {
+ constructor(private readonly removalPolicy: RemovalPolicy) {}
+
visit(node: IConstruct): void {
if (!(node instanceof CfnResource)) {
return;
@@ -51,7 +50,7 @@ class ApplyRemovalPolicyAspect implements IAspect {
const resourceType = node.cfnResourceType;
- const retainedTypes = [
+ const infrastructureTypes = [
'AWS::KMS::Key',
'AWS::RDS::DBCluster',
'AWS::RDS::DBInstance',
@@ -64,7 +63,7 @@ class ApplyRemovalPolicyAspect implements IAspect {
'AWS::SecretsManager::SecretTargetAttachment',
];
- const destroyedTypes = [
+ const ephemeralTypes = [
'AWS::CDK::Metadata',
'AWS::CloudFormation::Stack',
'AWS::Lambda::Function',
@@ -76,9 +75,9 @@ class ApplyRemovalPolicyAspect implements IAspect {
'AWS::Logs::LogGroup',
];
- if (retainedTypes.includes(resourceType)) {
- node.applyRemovalPolicy(RemovalPolicy.RETAIN);
- } else if (destroyedTypes.includes(resourceType)) {
+ if (infrastructureTypes.includes(resourceType)) {
+ node.applyRemovalPolicy(this.removalPolicy);
+ } else if (ephemeralTypes.includes(resourceType)) {
node.applyRemovalPolicy(RemovalPolicy.DESTROY);
}
// For Custom:: resources and anything else, let CDK defaults apply
@@ -150,12 +149,14 @@ export interface AuroraProps {
*/
readonly proxySecurityGroups?: aws_ec2.ISecurityGroup[];
/**
- * @deprecated This property is no longer used. Removal policies are now automatically
- * set based on resource type: infrastructure resources (KMS, RDS, Security Groups, Secrets)
- * are RETAINED for safety, while ephemeral resources (Lambdas, IAM, nested stacks) are DESTROYED.
- * This enables safe CDK->Terragrunt migrations.
+ * Removal policy for infrastructure resources (RDS cluster, instances, secrets, security groups).
+ *
+ * Use `RemovalPolicy.RETAIN` for production databases to prevent accidental deletion.
+ * Use `RemovalPolicy.DESTROY` for development/QA environments to enable automatic cleanup.
+ *
+ * Ephemeral resources (lambdas, IAM roles, log groups) are always DESTROYED regardless of this setting.
*
- * @default - Automatic based on resource type
+ * @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy;
/**
@@ -706,16 +707,9 @@ export class Aurora extends Construct {
}
// Apply removal policies based on resource type
- // Infrastructure resources (KMS, RDS, Security Groups, Secrets) are RETAINED
- // Ephemeral resources (Lambdas, IAM, nested stacks) are DESTROYED
- Aspects.of(this).add(new ApplyRemovalPolicyAspect());
-
- // Warn if deprecated removalPolicy prop is used
- if (props.removalPolicy !== undefined) {
- Annotations.of(this).addWarning(
- 'The removalPolicy property is deprecated and no longer has any effect. ' +
- 'Removal policies are now automatically set based on resource type.',
- );
- }
+ // Infrastructure resources use the configurable removal policy (default: DESTROY)
+ // Ephemeral resources (Lambdas, IAM, nested stacks) are always DESTROYED
+ const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY;
+ Aspects.of(this).add(new ApplyRemovalPolicyAspect(removalPolicy));
}
}
diff --git a/test/aurora.test.ts b/test/aurora.test.ts
index 4d6fb18..c475077 100644
--- a/test/aurora.test.ts
+++ b/test/aurora.test.ts
@@ -147,7 +147,7 @@ describe('Aurora', () => {
it('proxyName', () => {
template.hasResourceProperties('AWS::RDS::DBProxy', { DBProxyName: 'Test' });
});
- it('removalPolicy defaults to RETAIN for infrastructure resources', () => {
+ it('removalPolicy defaults to DESTROY for infrastructure resources', () => {
const templateJson = template.toJSON();
const resources = templateJson.Resources || {};
@@ -168,7 +168,7 @@ describe('Aurora', () => {
expect(infrastructureResources.length).toBeGreaterThan(0);
infrastructureResources.forEach(([, resource]: [string, any]) => {
- expect(resource.DeletionPolicy).toBe('Retain');
+ expect(['Delete', undefined]).toContain(resource.DeletionPolicy);
});
});
it('retention', () => {
@@ -349,13 +349,30 @@ describe('Aurora', () => {
]);
expect(aurora.proxySecurityGroups).toStrictEqual([sg]);
});
- it('removalPolicy is deprecated and warns when used', () => {
- createAurora({ ...defaultAuroraProps, removalPolicy: RemovalPolicy.DESTROY });
- // Should warn that the property is deprecated
- annotations.hasWarning('*', Match.stringLikeRegexp('removalPolicy property is deprecated'));
- // Should still apply automatic policies (infrastructure retained, ephemeral destroyed)
- template.hasResource('AWS::RDS::DBCluster', {
- DeletionPolicy: 'Retain',
+ it('removalPolicy RETAIN applies to infrastructure resources', () => {
+ createAurora({ ...defaultAuroraProps, removalPolicy: RemovalPolicy.RETAIN });
+
+ const templateJson = template.toJSON();
+ const resources = templateJson.Resources || {};
+
+ const infrastructureTypes = [
+ 'AWS::RDS::DBCluster',
+ 'AWS::RDS::DBInstance',
+ 'AWS::SecretsManager::Secret',
+ 'AWS::RDS::DBClusterParameterGroup',
+ 'AWS::EC2::SecurityGroup',
+ 'AWS::EC2::SecurityGroupEgress',
+ 'AWS::EC2::SecurityGroupIngress',
+ ];
+
+ const infrastructureResources = Object.entries(resources).filter(([_, resource]: [string, any]) =>
+ infrastructureTypes.includes(resource.Type),
+ );
+
+ expect(infrastructureResources.length).toBeGreaterThan(0);
+
+ infrastructureResources.forEach(([, resource]: [string, any]) => {
+ expect(resource.DeletionPolicy).toBe('Retain');
});
});
it('retention', () => {
@@ -457,8 +474,8 @@ describe('Aurora', () => {
});
});
- describe('automatic removal policy aspect', () => {
- it('applies RETAIN to infrastructure resources', () => {
+ describe('configurable removal policy', () => {
+ it('defaults to DESTROY for infrastructure resources', () => {
createAurora({ ...defaultAuroraProps });
const templateJson = template.toJSON();
@@ -482,9 +499,9 @@ describe('Aurora', () => {
// Verify we found infrastructure resources
expect(infrastructureResources.length).toBeGreaterThan(0);
- // Check that these resources have DeletionPolicy: Retain
+ // Check that these resources have DeletionPolicy: Delete or undefined (which defaults to Delete)
infrastructureResources.forEach(([, resource]: [string, any]) => {
- expect(resource.DeletionPolicy).toBe('Retain');
+ expect(['Delete', undefined]).toContain(resource.DeletionPolicy);
});
});