Repository: pulumi/pulumi-cdk (ts)

Issue:
Support `removalPolicy` for stateful resources
## Hello!
<!-- Please leave this section as-is, it's designed to help others in the community know how to interact with our GitHub issues. -->

- Vote on this issue by adding a 👍 reaction
- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)

## Issue details

<!-- Enhancement requests are most helpful when they describe the problem you're having as well as articulating the potential solution you'd like to see built. -->

Many stateful resources in the AWS Construct Library accept a `removalPolicy` as a property, typically defaulting it to `RETAIN`.

If the AWS Construct Library resource does not accept a removalPolicy argument, users can configure it by using the escape hatch mechanism, as shown in the following example:
```
cfn_bucket = bucket.node.find_child("Resource")
cfn_bucket.apply_removal_policy(RemovalPolicy.DESTROY)
```

Right now pulumi-cdk doesn't respect the removal policy and always deletes the resources.

### Affected area/feature

<!-- If you know the specific area where this feature request would go (e.g. Automation API, the Pulumi Service, the Terraform bridge, etc.), feel free to put that area here.  -->


Pull request:
CDK RemovalPolicy maps to Pulumi retainOnDelete
This PR maps CloudFormation [DeletionPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html)
to Pulumi `retainOnDelete`.

**`DeletionPolicy` => `retainOnDelete`**

- `Delete` => `false`
- `Retain` => `true`
- `RetainExceptOnCreate` => `true`
- `Snapshot` => `true`

Looking specifically for validation on `Snapshot` mapping. This
functionality does not exist in Pulumi so I decided to err on the side
of caution and retain the resource instead of deleting it. We log a
warning in this case and they should see the retention in the preview.

closes #188

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e0d4f13..5aa3f49 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,5 +1,5 @@
 env:
-  AWS_REGION: us-west-2
+  AWS_REGION: us-east-2
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
   NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cd0ed45..bfca341 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,5 +1,5 @@
 env:
-  AWS_REGION: us-west-2
+  AWS_REGION: us-east-2
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
   NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/examples/cloudfront-lambda-urls/index.ts b/examples/cloudfront-lambda-urls/index.ts
index cef3924..46cc738 100644
--- a/examples/cloudfront-lambda-urls/index.ts
+++ b/examples/cloudfront-lambda-urls/index.ts
@@ -12,6 +12,7 @@ import {
 import { FunctionUrlOrigin, S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';
 import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
 import { Bucket } from 'aws-cdk-lib/aws-s3';
+import { RemovalPolicy } from 'aws-cdk-lib';
 
 class CloudFrontAppStack extends pulumicdk.Stack {
     public cloudFrontUrl: pulumi.Output<string>;
@@ -32,7 +33,9 @@ class CloudFrontAppStack extends pulumicdk.Stack {
             authType: FunctionUrlAuthType.NONE,
         });
 
-        const bucket = new Bucket(this, 'Bucket');
+        const bucket = new Bucket(this, 'Bucket', {
+            removalPolicy: RemovalPolicy.DESTROY,
+        });
 
         const distro = new Distribution(this, 'distro', {
             defaultBehavior: {
diff --git a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts
index ccf009b..c4585af 100644
--- a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts
+++ b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts
@@ -27,6 +27,7 @@ export class S3ObjectLambdaStack extends pulumicdk.Stack {
             accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
             encryption: s3.BucketEncryption.S3_MANAGED,
             blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
+            removalPolicy: cdk.RemovalPolicy.DESTROY,
         });
 
         // Delegating access control to access points
diff --git a/go.mod b/go.mod
index c3a7a5a..c2ca990 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,10 @@ module github.com/pulumi/pulumi-cdk
 go 1.22.5
 
 require (
+	github.com/aws/aws-sdk-go v1.50.36
+	github.com/aws/aws-sdk-go-v2/config v1.27.11
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0
+	github.com/aws/smithy-go v1.22.0
 	github.com/gorilla/websocket v1.5.3
 	github.com/pulumi/pulumi/pkg/v3 v3.137.0
 	github.com/stretchr/testify v1.9.0
@@ -31,21 +35,22 @@ require (
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
-	github.com/aws/aws-sdk-go v1.50.36 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
-	github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.32.4 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/kms v1.30.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
-	github.com/aws/smithy-go v1.20.2 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/blang/semver v3.5.1+incompatible // indirect
 	github.com/cenkalti/backoff/v3 v3.2.2 // indirect
diff --git a/go.sum b/go.sum
index 28c4fe2..e355034 100644
--- a/go.sum
+++ b/go.sum
@@ -53,10 +53,10 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aws/aws-sdk-go v1.50.36 h1:PjWXHwZPuTLMR1NIb8nEjLucZBMzmf84TLoLbD8BZqk=
 github.com/aws/aws-sdk-go v1.50.36/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
-github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
-github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
+github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE=
+github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
 github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
 github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
@@ -65,36 +65,36 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYh
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 h1:7Zwtt/lP3KNRkeZre7soMELMGNoBrutx8nobg1jKWmo=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15/go.mod h1:436h2adoHb57yd+8W+gYPrrA9U/R/SuAuOO42Ushzhw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk=
 github.com/aws/aws-sdk-go-v2/service/iam v1.31.4 h1:eVm30ZIDv//r6Aogat9I88b5YX1xASSLcEDqHYRPVl0=
 github.com/aws/aws-sdk-go-v2/service/iam v1.31.4/go.mod h1:aXWImQV0uTW35LM0A/T4wEg6R1/ReXUu4SM6/lUHYK0=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI=
 github.com/aws/aws-sdk-go-v2/service/kms v1.30.1 h1:SBn4I0fJXF9FYOVRSVMWuhvEKoAHDikjGpS3wlmw5DE=
 github.com/aws/aws-sdk-go-v2/service/kms v1.30.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0 h1:SwaJ0w0MOp0pBTIKTamLVeTKD+iOWyNJRdJ2KCQRg6Q=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0=
 github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
 github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
 github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
 github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
-github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
-github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
+github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
diff --git a/integration/cloudfront/index.ts b/integration/cloudfront/index.ts
index c8b7677..e248402 100644
--- a/integration/cloudfront/index.ts
+++ b/integration/cloudfront/index.ts
@@ -4,13 +4,15 @@ import * as s3 from 'aws-cdk-lib/aws-s3';
 import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
 import * as pulumicdk from '@pulumi/cdk';
 import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
-import { Duration } from 'aws-cdk-lib';
+import { Duration, RemovalPolicy } from 'aws-cdk-lib';
 
 class CloudFrontStack extends pulumicdk.Stack {
     public readonly bucketName: pulumi.Output<string>;
     constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
         super(app, id, options);
-        const bucket = new s3.Bucket(this, 'Bucket');
+        const bucket = new s3.Bucket(this, 'Bucket', {
+            removalPolicy: RemovalPolicy.DESTROY,
+        });
         this.bucketName = this.asOutput(bucket.bucketName);
         const cachePolicy = new cloudfront.CachePolicy(this, 'CachePolicy', {
             maxTtl: Duration.days(1),
diff --git a/integration/removal-policy/Pulumi.yaml b/integration/removal-policy/Pulumi.yaml
new file mode 100644
index 0000000..ff9c7e2
--- /dev/null
+++ b/integration/removal-policy/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: pulumi-aws-removal-policy
+runtime: nodejs
+description: removal-policy integration test
diff --git a/integration/removal-policy/index.ts b/integration/removal-policy/index.ts
new file mode 100644
index 0000000..01649e4
--- /dev/null
+++ b/integration/removal-policy/index.ts
@@ -0,0 +1,20 @@
+import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as pulumicdk from '@pulumi/cdk';
+import { RemovalPolicy } from 'aws-cdk-lib';
+import { Config } from '@pulumi/pulumi';
+const config = new Config();
+const bucketName = config.require('bucketName');
+
+class RemovalPolicyStack extends pulumicdk.Stack {
+    constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
+        super(app, id, options);
+        new s3.Bucket(this, 'testbucket', {
+            bucketName: bucketName,
+            removalPolicy: RemovalPolicy.RETAIN,
+        });
+    }
+}
+
+new pulumicdk.App('app', (scope: pulumicdk.App) => {
+    new RemovalPolicyStack(scope, 'teststack');
+});
diff --git a/integration/removal-policy/package.json b/integration/removal-policy/package.json
new file mode 100644
index 0000000..06206ee
--- /dev/null
+++ b/integration/removal-policy/package.json
@@ -0,0 +1,15 @@
+{
+    "name": "pulumi-aws-cdk",
+    "devDependencies": {
+        "@types/node": "^10.0.0"
+    },
+    "dependencies": {
+        "@pulumi/aws": "^6.0.0",
+        "@pulumi/aws-native": "^1.8.0",
+        "@pulumi/cdk": "^0.5.0",
+        "@pulumi/pulumi": "^3.0.0",
+        "aws-cdk-lib": "2.156.0",
+        "constructs": "10.3.0",
+        "esbuild": "^0.24.0"
+    }
+}
diff --git a/integration/removal-policy/step2/Pulumi.yaml b/integration/removal-policy/step2/Pulumi.yaml
new file mode 100644
index 0000000..ff9c7e2
--- /dev/null
+++ b/integration/removal-policy/step2/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: pulumi-aws-removal-policy
+runtime: nodejs
+description: removal-policy integration test
diff --git a/integration/removal-policy/step2/index.ts b/integration/removal-policy/step2/index.ts
new file mode 100644
index 0000000..f7be988
--- /dev/null
+++ b/integration/removal-policy/step2/index.ts
@@ -0,0 +1,20 @@
+import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as pulumicdk from '@pulumi/cdk';
+import { RemovalPolicy } from 'aws-cdk-lib';
+import { Config } from '@pulumi/pulumi';
+const config = new Config();
+const bucketName = config.require('bucketName');
+
+class RemovalPolicyStack extends pulumicdk.Stack {
+    constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
+        super(app, id, options);
+        new s3.Bucket(this, 'testbucket', {
+            bucketName: bucketName,
+            removalPolicy: RemovalPolicy.DESTROY,
+        });
+    }
+}
+
+new pulumicdk.App('app', (scope: pulumicdk.App) => {
+    new RemovalPolicyStack(scope, 'teststack');
+});
diff --git a/integration/removal-policy/step2/package.json b/integration/removal-policy/step2/package.json
new file mode 100644
index 0000000..06206ee
--- /dev/null
+++ b/integration/removal-policy/step2/package.json
@@ -0,0 +1,15 @@
+{
+    "name": "pulumi-aws-cdk",
+    "devDependencies": {
+        "@types/node": "^10.0.0"
+    },
+    "dependencies": {
+        "@pulumi/aws": "^6.0.0",
+        "@pulumi/aws-native": "^1.8.0",
+        "@pulumi/cdk": "^0.5.0",
+        "@pulumi/pulumi": "^3.0.0",
+        "aws-cdk-lib": "2.156.0",
+        "constructs": "10.3.0",
+        "esbuild": "^0.24.0"
+    }
+}
diff --git a/integration/removal-policy/step2/tsconfig.json b/integration/removal-policy/step2/tsconfig.json
new file mode 100644
index 0000000..eac442c
--- /dev/null
+++ b/integration/removal-policy/step2/tsconfig.json
@@ -0,0 +1,18 @@
+{
+    "compilerOptions": {
+        "strict": true,
+        "outDir": "bin",
+        "target": "es2019",
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "pretty": true,
+        "noFallthroughCasesInSwitch": true,
+        "noImplicitReturns": true,
+        "forceConsistentCasingInFileNames": true
+    },
+    "include": [
+        "./*.ts"
+    ]
+}
diff --git a/integration/removal-policy/tsconfig.json b/integration/removal-policy/tsconfig.json
new file mode 100644
index 0000000..eac442c
--- /dev/null
+++ b/integration/removal-policy/tsconfig.json
@@ -0,0 +1,18 @@
+{
+    "compilerOptions": {
+        "strict": true,
+        "outDir": "bin",
+        "target": "es2019",
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "pretty": true,
+        "noFallthroughCasesInSwitch": true,
+        "noImplicitReturns": true,
+        "forceConsistentCasingInFileNames": true
+    },
+    "include": [
+        "./*.ts"
+    ]
+}
diff --git a/integration/secretsmanager/index.ts b/integration/secretsmanager/index.ts
index 3b6f7aa..32a1716 100644
--- a/integration/secretsmanager/index.ts
+++ b/integration/secretsmanager/index.ts
@@ -4,6 +4,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as iam from 'aws-cdk-lib/aws-iam';
 import * as secrets from 'aws-cdk-lib/aws-secretsmanager';
 import * as pulumicdk from '@pulumi/cdk';
+import { RemovalPolicy } from 'aws-cdk-lib';
 
 class SecretsManagerStack extends pulumicdk.Stack {
     constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
@@ -27,6 +28,7 @@ class SecretsManagerStack extends pulumicdk.Stack {
             }),
             vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }),
             credentials: rds.Credentials.fromGeneratedSecret('admin'),
+            removalPolicy: RemovalPolicy.DESTROY,
         });
 
         const role = new iam.Role(this, 'Role', {
diff --git a/src/cfn.ts b/src/cfn.ts
index e7eb61a..95b8ff3 100644
--- a/src/cfn.ts
+++ b/src/cfn.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { CfnDeletionPolicy } from 'aws-cdk-lib/core';
+
 export interface CloudFormationParameter {
     readonly Type: string;
     readonly Default?: any;
@@ -21,6 +23,7 @@ export interface CloudFormationResource {
     readonly Type: string;
     readonly Properties: any;
     readonly Condition?: string;
+    readonly DeletionPolicy?: CfnDeletionPolicy;
     readonly DependsOn?: string | string[];
 }
 
diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts
index fff2075..25c60c6 100644
--- a/src/converters/app-converter.ts
+++ b/src/converters/app-converter.ts
@@ -6,7 +6,7 @@ import { ConstructInfo, Graph, GraphBuilder, GraphNode } from '../graph';
 import { ArtifactConverter } from './artifact-converter';
 import { lift, Mapping, AppComponent } from '../types';
 import { CdkConstruct, ResourceAttributeMapping, ResourceMapping } from '../interop';
-import { debug } from '@pulumi/pulumi/log';
+import { debug, warn } from '@pulumi/pulumi/log';
 import {
     cidr,
     getAccountId,
@@ -141,7 +141,7 @@ export class StackConverter extends ArtifactConverter {
                 debug(`Processing node with template: ${JSON.stringify(cfn)}`);
                 debug(`Creating resource for ${n.logicalId}`);
                 const props = this.processIntrinsics(cfn.Properties);
-                const options = this.processOptions(cfn, parent);
+                const options = this.processOptions(n.logicalId, cfn, parent);
 
                 const mapped = this.mapResource(n.logicalId, cfn.Type, props, options);
                 this.registerResource(mapped, n);
@@ -301,10 +301,46 @@ export class StackConverter extends ArtifactConverter {
         return cfnMapping;
     }
 
-    private processOptions(resource: CloudFormationResource, parent: pulumi.Resource): pulumi.ResourceOptions {
+    /**
+     * Converts a CloudFormation deletion policy to a Pulumi retainOnDelete value.
+     *
+     * When a CloudFormation resource is set to Snapshot, CloudFormation will first
+     * create a snapshot of the resource before deleting it. Pulumi does not have the same
+     * capability, which means the user would need to manually create the snapshot before deleting.
+     * to be on the safe side, we will retain the resource
+     *
+     * @param logicalId - The logicalId of the resource
+     * @param resource - The CloudFormation resource
+     * @returns - The retainOnDelete value
+     */
+    private getRetainOnDelete(logicalId: string, resource: CloudFormationResource): boolean | undefined {
+        if (resource.DeletionPolicy === undefined) {
+            return undefined;
+        }
+        switch (resource.DeletionPolicy) {
+            case cdk.CfnDeletionPolicy.DELETE:
+                return false;
+            case cdk.CfnDeletionPolicy.RETAIN:
+            case cdk.CfnDeletionPolicy.RETAIN_EXCEPT_ON_CREATE:
+                // RETAIN_EXCEPT_ON_CREATE only applies to CloudFormation because CloudFormation will rollback a stack
+                // if it fails to deploy. Pulumi does not have the same behavior, so we will treat it as RETAIN
+                return true;
+            case cdk.CfnDeletionPolicy.SNAPSHOT:
+                warn(`DeletionPolicy Snapshot is not supported. Resource '${logicalId}' will be retained.`);
+                return true;
+        }
+    }
+
+    private processOptions(
+        logicalId: string,
+        resource: CloudFormationResource,
+        parent: pulumi.Resource,
+    ): pulumi.ResourceOptions {
         const dependsOn = getDependsOn(resource);
+        const retainOnDelete = this.getRetainOnDelete(logicalId, resource);
         return {
             parent: parent,
+            retainOnDelete,
             dependsOn: dependsOn?.flatMap((id) => {
                 const resource = this.resources.get(id);
                 if (resource === undefined) {


Test patch:
diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml
index b2e198e..71db8db 100644
--- a/.github/workflows/acceptance-tests.yml
+++ b/.github/workflows/acceptance-tests.yml
@@ -7,7 +7,7 @@ on:
         description: The folder in which to run tests
 
 env:
-  AWS_REGION: us-west-2
+  AWS_REGION: us-east-2
 
 jobs:
   acceptance-tests:
diff --git a/.github/workflows/run-acceptance-tests.yml b/.github/workflows/run-acceptance-tests.yml
index fb0a8a6..15d39ee 100644
--- a/.github/workflows/run-acceptance-tests.yml
+++ b/.github/workflows/run-acceptance-tests.yml
@@ -1,5 +1,5 @@
 env:
-  AWS_REGION: us-west-2
+  AWS_REGION: us-east-2
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
   NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/integration/examples_nodejs_test.go b/integration/examples_nodejs_test.go
index d6be63f..5992ac8 100644
--- a/integration/examples_nodejs_test.go
+++ b/integration/examples_nodejs_test.go
@@ -16,10 +16,18 @@ package examples
 
 import (
 	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"math/rand"
 	"path/filepath"
 	"testing"
 	"time"
 
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
+	"github.com/aws/aws-sdk-go-v2/service/s3/types"
+	"github.com/aws/smithy-go"
 	"github.com/pulumi/pulumi/pkg/v3/testing/integration"
 	"github.com/stretchr/testify/assert"
 )
@@ -175,6 +183,62 @@ func TestReplaceOnChanges(t *testing.T) {
 	integration.ProgramTest(t, &test)
 }
 
+func TestRemovalPolicy(t *testing.T) {
+	// Since we are creating two tests we have to set `NoParallel` on each test
+	// and set parallel here.
+	t.Parallel()
+	ctx := context.Background()
+	config, err := config.LoadDefaultConfig(ctx)
+	assert.NoError(t, err)
+	client := s3.NewFromConfig(config)
+
+	suffix := rand.Intn(10000)
+	bucketName := fmt.Sprintf("pulumi-cdk-removal-test-%d", suffix)
+	t.Logf("Bucket name: %s", bucketName)
+
+	testConfig := map[string]string{
+		"bucketName": bucketName,
+	}
+
+	// ----------------------------------------------------------
+	// Step 1: Create a bucket with a removal policy of 'retain'
+	// ----------------------------------------------------------
+	test1 := getJSBaseOptions(t).
+		With(integration.ProgramTestOptions{
+			Dir:        filepath.Join(getCwd(t), "removal-policy"),
+			NoParallel: true,
+			Config:     testConfig,
+		})
+
+	integration.ProgramTest(t, &test1)
+
+	// Assert that the bucket still exists
+	exists, err := bucketExists(ctx, client, bucketName)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	// Delete the bucket before Step 2.
+	_, err = client.DeleteBucket(ctx, &s3.DeleteBucketInput{
+		Bucket: &bucketName,
+	})
+	assert.NoError(t, err)
+
+	// ----------------------------------------------------------
+	// Step 2: Create a new stack with the same bucket name and a removal policy of 'destroy'
+	// ----------------------------------------------------------
+	test2 := getJSBaseOptions(t).With(integration.ProgramTestOptions{
+		Dir:        filepath.Join(getCwd(t), "removal-policy/step2"),
+		NoParallel: true,
+		Config:     testConfig,
+	})
+	integration.ProgramTest(t, &test2)
+
+	// Assert that the bucket no longer exists
+	exists, err = bucketExists(ctx, client, bucketName)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+}
+
 func getJSBaseOptions(t *testing.T) integration.ProgramTestOptions {
 	base := getBaseOptions(t)
 	baseJS := base.With(integration.ProgramTestOptions{
@@ -185,3 +249,20 @@ func getJSBaseOptions(t *testing.T) integration.ProgramTestOptions {
 
 	return baseJS
 }
+
+func bucketExists(ctx context.Context, client *s3.Client, bucketName string) (bool, error) {
+	_, err := client.HeadBucket(ctx, &s3.HeadBucketInput{
+		Bucket: &bucketName,
+	})
+	if err != nil {
+		var apiError smithy.APIError
+		if errors.As(err, &apiError) {
+			switch apiError.(type) {
+			case *types.NotFound:
+				return false, nil
+			}
+		}
+		return false, err
+	}
+	return true, nil
+}
diff --git a/tests/cfn-resource-mappings.test.ts b/tests/cfn-resource-mappings.test.ts
index 636270d..ae4eb03 100644
--- a/tests/cfn-resource-mappings.test.ts
+++ b/tests/cfn-resource-mappings.test.ts
@@ -1,7 +1,5 @@
-import { CustomResource } from '@pulumi/pulumi';
 import { mapToCfnResource } from '../src/cfn-resource-mappings';
 import * as aws from '@pulumi/aws-native';
-import { moduleName, typeName } from '../src/naming';
 
 class MockResource {
     constructor(args: { [key: string]: any }) {
diff --git a/tests/options.test.ts b/tests/options.test.ts
new file mode 100644
index 0000000..068b67d
--- /dev/null
+++ b/tests/options.test.ts
@@ -0,0 +1,257 @@
+import * as pulumi from '@pulumi/pulumi';
+import { StackManifest } from '../src/assembly';
+import { StackConverter } from '../src/converters/app-converter';
+import { MockAppComponent } from './mocks';
+import { CfnDeletionPolicy } from 'aws-cdk-lib';
+jest.mock('@pulumi/pulumi', () => {
+    return {
+        ...jest.requireActual('@pulumi/pulumi'),
+        CustomResource: jest.fn().mockImplementation(() => {
+            return {};
+        }),
+    };
+});
+
+afterAll(() => {
+    jest.resetAllMocks();
+});
+
+beforeEach(() => {
+    jest.clearAllMocks();
+});
+
+describe('options', () => {
+    test('retainOnDelete true when DeletionPolicy=Retain', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        DeletionPolicy: CfnDeletionPolicy.RETAIN,
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.objectContaining({
+                retainOnDelete: true,
+            }),
+        );
+    });
+
+    test('retainOnDelete true when DeletionPolicy=RetainExceptOnCreate', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        DeletionPolicy: CfnDeletionPolicy.RETAIN_EXCEPT_ON_CREATE,
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.objectContaining({
+                retainOnDelete: true,
+            }),
+        );
+    });
+
+    test('retainOnDelete false when DeletionPolicy=Delete', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        DeletionPolicy: CfnDeletionPolicy.DELETE,
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.objectContaining({
+                retainOnDelete: false,
+            }),
+        );
+    });
+
+    test('retainOnDelete true when DeletionPolicy=Snapshot', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        DeletionPolicy: CfnDeletionPolicy.SNAPSHOT,
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.objectContaining({
+                retainOnDelete: true,
+            }),
+        );
+    });
+
+    test('retainOnDelete not set', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.not.objectContaining({
+                retainOnDelete: false,
+            }),
+        );
+    });
+});