Repository: pulumi/pulumi-cdk
Pull request #190 (2024-10-31 16:30:38)

Add support for CDK Custom Resources
This PR adds support for [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) to pulumi-cdk. It implements does so by using the `CustomResourceEmulator` resource from [aws-native](https://www.pulumi.com/registry/packages/aws-native/api-docs/cloudformation/customresourceemulator/).

For the first implementation we decided to limit the scope to Lambda backed Custom Resources, because the SNS variants are not widely used.

I'd recommend reviewing in this order:
- `src/graph.ts`and `src/converters/app-converter.ts`. The changes in these files ensure that Custom Resources get correctly parsed and other resources can reference their attributes with the `GetAtt` intrinsic
- `src/cfn-resource-mappings.ts`: This constructs the `CustomResourceEmulator` based on the CDK inputs while re-using the staging bucket to store the CustomResource responses.
- unit & integration tests

**Noteworthy**:
I added a temporary workaround for shortening the resource names until https://github.com/pulumi/pulumi-aws-native/issues/1816 is resolved. It can be toggled on by setting the `PULUMI_CDK_EXPERIMENTAL_MAX_NAME_LENGTH` env variable. Without this none of the CustomResources worked because they have deeply nested Lambdas and IAM roles. Those resources have a max name limit of 64.

Closes https://github.com/pulumi/pulumi-cdk/issues/109
Closes https://github.com/pulumi/pulumi-cdk/issues/60


Patch:
diff --git a/integration/custom-resource/Pulumi.yaml b/integration/custom-resource/Pulumi.yaml
new file mode 100644
index 0000000..5ecd190
--- /dev/null
+++ b/integration/custom-resource/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: pulumi-custom-resource
+runtime: nodejs
+description: CDK custom resource example
diff --git a/integration/custom-resource/index.ts b/integration/custom-resource/index.ts
new file mode 100644
index 0000000..19b26f2
--- /dev/null
+++ b/integration/custom-resource/index.ts
@@ -0,0 +1,82 @@
+import * as pulumi from '@pulumi/pulumi';
+import * as pulumicdk from '@pulumi/cdk';
+
+import * as cdk from 'aws-cdk-lib';
+import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as iam from 'aws-cdk-lib/aws-iam';
+import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
+
+class S3DeploymentStack extends pulumicdk.Stack {
+    bucketWebsiteUrl: pulumi.Output<string>;
+    bucketObjectKeys: pulumi.Output<string[]>;
+
+    constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
+        super(app, id, options);
+
+        const bucket = new s3.Bucket(this, 'WebsiteBucket', {
+            websiteIndexDocument: 'index.html',
+            publicReadAccess: true,
+            blockPublicAccess: {
+                blockPublicAcls: false,
+                blockPublicPolicy: false,
+                ignorePublicAcls: false,
+                restrictPublicBuckets: false,
+            },
+            removalPolicy: cdk.RemovalPolicy.DESTROY,
+            autoDeleteObjects: true,
+        });
+
+
+        this.bucketWebsiteUrl = this.asOutput(bucket.bucketWebsiteUrl);
+
+        const deploy = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
+            sources: [s3deploy.Source.data('index.html', 'Hello, World!')],
+            destinationBucket: bucket,
+        });
+
+        this.bucketObjectKeys = this.asOutput(deploy.objectKeys);
+
+        // Create a role that can read from the deployment bucket
+        // This verifies that GetAtt works on Custom Resources
+        new iam.Role(this, 'CustomResourceRole', {
+            assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+            inlinePolicies: {
+                'CustomResourcePolicy': new iam.PolicyDocument({
+                    statements: [
+                        new iam.PolicyStatement({
+                            actions: ['s3:GetObject'],
+                            resources: [`${deploy.deployedBucket.bucketArn}/*`],
+                        }),
+                    ],
+                }),
+            }
+        });
+    }
+}
+
+const cfg = new pulumi.Config();
+const accountId = cfg.require('accountId');
+
+class MyApp extends pulumicdk.App {
+    constructor() {
+        super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
+            const stack = new S3DeploymentStack(scope, 's3deployment', {
+                // configure the environment to prevent the bucket from using the unsupported FindInMap intrinsic (TODO[pulumi/pulumi-cdk#187])
+                props: {
+                    env: {
+                        account: accountId,
+                        region: process.env.AWS_REGION,
+                    }
+                }
+            });
+            return {
+                bucketWebsiteUrl: stack.bucketWebsiteUrl,
+                bucketObjectKeys: stack.bucketObjectKeys
+            };
+        });
+    }
+}
+
+const app = new MyApp();
+export const websiteUrl = app.outputs['bucketWebsiteUrl'];
+export const objectKeys = app.outputs['bucketObjectKeys'];
diff --git a/integration/custom-resource/package.json b/integration/custom-resource/package.json
new file mode 100644
index 0000000..a1e9f74
--- /dev/null
+++ b/integration/custom-resource/package.json
@@ -0,0 +1,17 @@
+{
+    "name": "pulumi-aws-cdk",
+    "devDependencies": {
+        "@types/node": "^10.0.0"
+    },
+    "dependencies": {
+        "@pulumi/aws": "^6.0.0",
+        "@pulumi/aws-native": "^1.0.0",
+        "@pulumi/pulumi": "^3.0.0",
+        "aws-cdk-lib": "2.149.0",
+        "constructs": "10.3.0",
+        "@pulumi/cdk": "^0.5.0",
+        "@aws-sdk/client-s3": "^3.677.0",
+        "@aws-sdk/s3-request-presigner": "^3.677.0",
+        "@aws-sdk/client-lambda": "^3.677.0"
+    }
+}
diff --git a/integration/custom-resource/tsconfig.json b/integration/custom-resource/tsconfig.json
new file mode 100644
index 0000000..ab65afa
--- /dev/null
+++ b/integration/custom-resource/tsconfig.json
@@ -0,0 +1,18 @@
+{
+    "compilerOptions": {
+        "strict": true,
+        "outDir": "bin",
+        "target": "es2016",
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "pretty": true,
+        "noFallthroughCasesInSwitch": true,
+        "noImplicitReturns": true,
+        "forceConsistentCasingInFileNames": true
+    },
+    "files": [
+        "index.ts"
+    ]
+}
diff --git a/src/assembly/types.ts b/src/assembly/types.ts
index f930422..30459f1 100644
--- a/src/assembly/types.ts
+++ b/src/assembly/types.ts
@@ -1,4 +1,3 @@
-import { ConstructInfo } from 'aws-cdk-lib/core/lib/private/runtime-info';
 import { Node } from 'aws-cdk-lib/core/lib/private/tree-metadata';
 import { DockerImageManifestEntry, FileManifestEntry } from 'cdk-assets';
 
diff --git a/src/cfn-resource-mappings.ts b/src/cfn-resource-mappings.ts
index 3618434..480135d 100644
--- a/src/cfn-resource-mappings.ts
+++ b/src/cfn-resource-mappings.ts
@@ -116,7 +116,19 @@ export function mapToCfnResource(
             const mName = moduleName(typeName).toLowerCase();
             const pType = pulumiTypeName(typeName);
             const awsModule = aws as any;
-            return new awsModule[mName][pType](logicalId, props, options);
+
+            // Workaround until TODO[pulumi/pulumi-aws-native#1816] is resolved.
+            // Not expected to be exposed to users
+            let name = logicalId;
+            const maxNameLength = process.env.PULUMI_CDK_EXPERIMENTAL_MAX_NAME_LENGTH;
+            if (maxNameLength) {
+                const maxLength = parseInt(maxNameLength, 10);
+                if (name.length > maxLength) {
+                    name = name.substring(0, maxLength);
+                }
+            }
+
+            return new awsModule[mName][pType](name, props, options);
         }
     }
 }
diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts
index 0d4824b..83b2fc2 100644
--- a/src/converters/app-converter.ts
+++ b/src/converters/app-converter.ts
@@ -1,4 +1,5 @@
 import * as cdk from 'aws-cdk-lib/core';
+import * as aws from '@pulumi/aws-native'
 import * as pulumi from '@pulumi/pulumi';
 import { AssemblyManifestReader, StackManifest } from '../assembly';
 import { ConstructInfo, Graph, GraphBuilder, GraphNode } from '../graph';
@@ -21,7 +22,8 @@ import { CloudFormationResource, getDependsOn } from '../cfn';
 import { OutputMap, OutputRepr } from '../output-map';
 import { parseSub } from '../sub';
 import { getPartition } from '@pulumi/aws-native/getPartition';
-import { processSecretsManagerReferenceValue, resolveSecretsManagerDynamicReference } from './secrets-manager-dynamic';
+import { mapToCustomResource } from '../custom-resource-mapping';
+import { processSecretsManagerReferenceValue } from './secrets-manager-dynamic';
 
 /**
  * AppConverter will convert all CDK resources into Pulumi resources.
@@ -288,6 +290,12 @@ export class StackConverter extends ArtifactConverter {
             return awsMapping;
         }
 
+        const customResourceMapping = mapToCustomResource(logicalId, typeName, props, options, this.cdkStack);
+        if (customResourceMapping !== undefined) {
+            debug(`mapped ${logicalId} to custom resource(s)`);
+            return customResourceMapping;
+        }
+
         const cfnMapping = mapToCfnResource(logicalId, typeName, props, options);
         debug(`mapped ${logicalId} to native AWS resource(s)`);
         return cfnMapping;
@@ -501,6 +509,9 @@ export class StackConverter extends ArtifactConverter {
         const map = <Mapping<pulumi.Resource>>mapping;
         if (map.attributes && 'id' in map.attributes) {
             return map.attributes.id;
+        } else if (aws.cloudformation.CustomResourceEmulator.isInstance(map.resource)) {
+            // Custom resources have a `physicalResourceId` that is used for Ref
+            return map.resource.physicalResourceId;
         }
         return (<pulumi.CustomResource>(<Mapping<pulumi.Resource>>mapping).resource).id;
     }
@@ -529,6 +540,19 @@ export class StackConverter extends ArtifactConverter {
         // If this resource has explicit attribute mappings, those mappings will use PascalCase, not camelCase.
         const propertyName = mapping.attributes !== undefined ? attribute : attributePropertyName(attribute);
 
+        // CFN CustomResources have a `data` property that contains the attributes. It is part of the response
+        // of the Lambda Function backing the Custom Resource.
+        if (aws.cloudformation.CustomResourceEmulator.isInstance(mapping.resource)) {
+            return mapping.resource.data.apply((attrs) => {
+                const descs = Object.getOwnPropertyDescriptors(attrs);
+                const d = descs[attribute];
+                if (!d) {
+                    throw new Error(`No attribute ${attribute} on custom resource ${logicalId}`);
+                }
+                return d.value;
+            });
+        }
+
         const descs = Object.getOwnPropertyDescriptors(mapping.attributes || mapping.resource);
         const d = descs[propertyName];
         if (!d) {
diff --git a/src/custom-resource-mapping.ts b/src/custom-resource-mapping.ts
new file mode 100644
index 0000000..6e20227
--- /dev/null
+++ b/src/custom-resource-mapping.ts
@@ -0,0 +1,74 @@
+// Copyright 2016-2024, Pulumi Corporation.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as pulumi from '@pulumi/pulumi';
+import * as aws from '@pulumi/aws-native';
+import { ResourceMapping } from './interop';
+import { Stack } from 'aws-cdk-lib/core';
+import { PulumiSynthesizerBase } from './synthesizer';
+import { debug } from '@pulumi/pulumi/log';
+
+export function mapToCustomResource(
+    logicalId: string,
+    typeName: string,
+    rawProps: any,
+    options: pulumi.ResourceOptions,
+    stack: Stack,
+): ResourceMapping | undefined {
+    debug(`mapToCustomResource typeName: ${typeName} props: ${JSON.stringify(rawProps)}`);
+
+    if (isCustomResource(typeName)) {
+        const synth = stack.synthesizer;
+        if (!(synth instanceof PulumiSynthesizerBase)) {
+            throw new Error(`Synthesizer of stack ${stack.node.id} does not support custom resources. It must inherit from ${PulumiSynthesizerBase.name}.`);
+        }
+
+        const stagingBucket = synth.getStagingBucket();
+        const stackId = stack.node.id;
+
+        return new aws.cloudformation.CustomResourceEmulator(logicalId, {
+            stackId: stack.node.id,
+            bucketName: stagingBucket,
+            bucketKeyPrefix: `${synth.getDeployTimePrefix()}pulumi/custom-resources/${stackId}/${logicalId}`,
+            serviceToken: rawProps.ServiceToken,
+            resourceType: typeName,
+            customResourceProperties: rawProps,
+        }, {
+            ...options,
+            customTimeouts: convertToCustomTimeouts(rawProps.ServiceTimeout),
+        });
+    }
+    
+    return undefined;
+}
+
+function convertToCustomTimeouts(seconds?: number): pulumi.CustomTimeouts | undefined {
+    if (seconds === undefined) {
+        return undefined;
+    }
+    const duration = `${seconds}s`;
+    return {
+        create: duration,
+        update: duration,
+        delete: duration,
+    };
+}
+
+/**
+ * Determines if the given type name corresponds to a custom resource.
+ * Custom resources either use AWS::CloudFormation::CustomResource or Custom::MyCustomResourceTypeName for the type.
+ */
+function isCustomResource(typeName: string): boolean {
+    return typeName === 'AWS::CloudFormation::CustomResource' || typeName.startsWith('Custom::');
+}
diff --git a/src/graph.ts b/src/graph.ts
index 467a087..1d73101 100644
--- a/src/graph.ts
+++ b/src/graph.ts
@@ -49,6 +49,13 @@ export interface ConstructInfo {
      * Will be undefined for the construct representing the `Stack`
      */
     parent?: ConstructInfo;
+
+    constructInfo?: ConstructMetadata;
+}
+
+export interface ConstructMetadata {
+    "fqn": string;
+    "version": string;
 }
 
 export interface GraphNode {
@@ -175,6 +182,7 @@ export class GraphBuilder {
             path: tree.path,
             type: tree.constructInfo ? typeFromFqn(tree.constructInfo.fqn) : tree.id,
             attributes: tree.attributes,
+            constructInfo: tree.constructInfo,
         };
         const node: GraphNode = {
             incomingEdges: new Set<GraphNode>(),
@@ -203,13 +211,33 @@ export class GraphBuilder {
             if (resource.Type === 'AWS::EC2::VPC') {
                 this.vpcNodes[node.logicalId] = { vpcNode: node, vpcCidrBlockNode: undefined };
             }
+        } else if (node.construct.constructInfo?.fqn === 'aws-cdk-lib.CfnResource') {
+            // If the construct is a CfnResource, then we need to treat it as a resource
+            const logicalId = this.stack.logicalIdForPath(tree.path);
+            const resource = this.stack.resourceWithLogicalId(logicalId);
+            node.resource = resource;
+            node.logicalId = logicalId;
+            this.cfnElementNodes.set(logicalId, node);
+
+            // Custom Resources do not map to types. E.g. Custom::Bucket should not map to Bucket
+            if (!GraphBuilder.isCustomResource(tree, parent)) {
+                node.construct.type = typeFromCfn(resource.Type);
+            }
         }
+
         this.constructNodes.set(construct, node);
         if (tree.children) {
             Object.values(tree.children).forEach((child) => this.parseTree(child, construct));
         }
     }
 
+    private static isCustomResource(node: ConstructTree, parent?: ConstructInfo): boolean {
+        // CDK CustomResource are exposed as a CfnResource with the ID "Default"
+        // If the parent construct has the fqn of CustomResource and the current tree node is the "Default" node
+        // then we need to treat it as a Custom Resource
+        return parent?.constructInfo?.fqn === 'aws-cdk-lib.CustomResource' && node.id === 'Default'
+    }
+
     private _build(): Graph {
         // passes
         // 1. collect all constructs into a map from construct name to DAG node, converting CFN elements to fragments
diff --git a/src/synthesizer.ts b/src/synthesizer.ts
index 48080ed..0a7ee42 100644
--- a/src/synthesizer.ts
+++ b/src/synthesizer.ts
@@ -101,6 +101,20 @@ export abstract class PulumiSynthesizerBase extends cdk.StackSynthesizer {
      * stack to ensure the staging assets are created first
      */
     public abstract readonly stagingStack: CdkConstruct;
+
+    /**
+     * Returns the name of the staging bucket that will be used to store assets
+     * and custom resource responses.
+     */
+    public abstract getStagingBucket(): pulumi.Input<string>;
+
+
+    /**
+     * Returns the S3 key prefix that will be used for deploy time assets.
+     */
+    public getDeployTimePrefix(): string {
+        return DEPLOY_TIME_PREFIX;
+    }
 }
 
 /**
@@ -421,6 +435,12 @@ export class PulumiSynthesizer extends PulumiSynthesizerBase implements cdk.IReu
         return this.stagingBucket;
     }
 
+    public getStagingBucket(): pulumi.Input<string> {
+        const bucket = this.getCreateBucket();
+        assertBound(bucket);
+        return bucket.bucket;
+    }
+
     /**
      * Produce a bound Stack Synthesizer for the given stack.
      *


Test patch:
diff --git a/integration/examples_nodejs_test.go b/integration/examples_nodejs_test.go
index 8abf56b..e143613 100644
--- a/integration/examples_nodejs_test.go
+++ b/integration/examples_nodejs_test.go
@@ -18,9 +18,13 @@ import (
 	"bytes"
 	"path/filepath"
 	"testing"
+	"time"
 
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/sts"
 	"github.com/pulumi/pulumi/pkg/v3/testing/integration"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestApiGateway(t *testing.T) {
@@ -132,6 +136,45 @@ func TestErrors(t *testing.T) {
 	assert.Containsf(t, buf.String(), "Error: Event Bus policy statements must have a sid", "Expected error message not found in pulumi up output")
 }
 
+// TestCustomResource tests that CloudFormation Custom Resources work as expected. The test deploys two custom resources. One for cleaning the
+// S3 bucket on delete and another for uploading the index.html file for a static website to the bucket.
+// The test validates that the website is deployed, displays the expected content and gets cleaned up on delete.
+func TestCustomResource(t *testing.T) {
+	sess := session.Must(session.NewSessionWithOptions(session.Options{
+		SharedConfigState: session.SharedConfigEnable,
+	}))
+	svc := sts.New(sess)
+
+	result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
+	require.NoError(t, err, "Failed to get AWS account ID")
+	accountId := *result.Account
+
+	test := getJSBaseOptions(t).
+		With(integration.ProgramTestOptions{
+			Dir: filepath.Join(getCwd(t), "custom-resource"),
+			Config: map[string]string{
+				"accountId": accountId,
+			},
+			// Workaround until TODO[pulumi/pulumi-aws-native#1816] is resolved.
+			Env: []string{"PULUMI_CDK_EXPERIMENTAL_MAX_NAME_LENGTH=56"},
+			ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+				t.Logf("Outputs: %v", stack.Outputs)
+				url := stack.Outputs["websiteUrl"].(string)
+				assert.NotEmpty(t, url)
+
+				// Validate that the index.html file is deployed
+				integration.AssertHTTPResultWithRetry(t, url, nil, 60*time.Second, func(body string) bool {
+					return assert.Equal(t, "Hello, World!", body, "Body should equal 'Hello, World!', got %s", body)
+				})
+
+				objectKeys := stack.Outputs["objectKeys"].([]interface{})
+				assert.NotEmpty(t, objectKeys)
+			},
+		})
+
+	integration.ProgramTest(t, &test)
+}
+
 func TestReplaceOnChanges(t *testing.T) {
 	test := getJSBaseOptions(t).
 		With(integration.ProgramTestOptions{
diff --git a/tests/cfn-custom-resource-mappings.test.ts b/tests/cfn-custom-resource-mappings.test.ts
new file mode 100644
index 0000000..2f794da
--- /dev/null
+++ b/tests/cfn-custom-resource-mappings.test.ts
@@ -0,0 +1,139 @@
+import { Stack } from 'aws-cdk-lib';
+import { mapToCustomResource } from '../src/custom-resource-mapping';
+import * as aws from '@pulumi/aws-native';
+import { MockSynth } from './mocks';
+import { typeName } from '../src/naming';
+
+jest.mock('@pulumi/aws-native', () => {
+    return {
+        cloudformation: {
+            CustomResourceEmulator: jest.fn().mockImplementation(() => {
+                return {};
+            }),
+        },
+    };
+});
+
+afterAll(() => {
+    jest.resetAllMocks();
+});
+
+beforeEach(() => {
+    jest.clearAllMocks();
+});
+
+describe('Custom Resource Mapping', () => {
+    test.each([['Custom::MyCustomResource'], ['AWS::CloudFormation::CustomResource']])('%s',
+        (cfnType) => {
+            // GIVEN
+            const logicalId = 'My-resource';
+            const serviceToken = 'arn:aws:lambda:us-west-2:123456789012:function:my-function';
+            const cfnProps = {
+                ServiceToken: serviceToken,
+                MyProperty: 'my-value',
+            };
+            const bucketName = 'my-bucket';
+            const prefix = 'my-prefix';
+
+            const stack = {
+                synthesizer: new MockSynth(bucketName, prefix),
+                node: {
+                    id: 'my-stack',
+                }
+            } as unknown as Stack;
+
+            // WHEN
+            mapToCustomResource(logicalId, cfnType, cfnProps, {}, stack);
+            // THEN
+            expect(aws.cloudformation.CustomResourceEmulator).toHaveBeenCalledWith(logicalId, {
+                stackId: stack.node.id,
+                bucketName,
+                bucketKeyPrefix: `${prefix}pulumi/custom-resources/${stack.node.id}/${logicalId}`,
+                serviceToken,
+                resourceType: cfnType,
+                customResourceProperties: cfnProps,
+            }, {});
+        },
+    );
+
+    test('with timeout', () => {
+        // GIVEN
+        const cfnType = 'Custom::MyCustomResource';
+        const logicalId = 'My-resource';
+        const serviceToken = 'arn:aws:lambda:us-west-2:123456789012:function:my-function';
+        const cfnProps = {
+            ServiceToken: serviceToken,
+            ServiceTimeout: 60,
+            MyProperty: 'my-value',
+        };
+        const bucketName = 'my-bucket';
+        const prefix = 'my-prefix';
+
+        const stack = {
+            synthesizer: new MockSynth(bucketName, prefix),
+            node: {
+                id: 'my-stack',
+            }
+        } as unknown as Stack;
+
+        // WHEN
+        mapToCustomResource(logicalId, cfnType, cfnProps, {}, stack);
+        // THEN
+        expect(aws.cloudformation.CustomResourceEmulator).toHaveBeenCalledWith(logicalId, {
+            stackId: stack.node.id,
+            bucketName,
+            bucketKeyPrefix: `${prefix}pulumi/custom-resources/${stack.node.id}/${logicalId}`,
+            serviceToken,
+            resourceType: cfnType,
+            customResourceProperties: cfnProps,
+        }, {
+            customTimeouts: {
+                create: '60s',
+                update: '60s',
+                delete: '60s',
+            }
+        });
+    });
+
+    test('Wrong Synthesizer', () => {
+        // GIVEN
+        const cfnType = 'Custom::MyCustomResource';
+        const logicalId = 'My-resource';
+        const serviceToken = 'arn:aws:lambda:us-west-2:123456789012:function:my-function';
+        const cfnProps = {
+            ServiceToken: serviceToken,
+            MyProperty: 'my-value',
+        };
+
+        const stack = {
+            synthesizer: {},
+            node: {
+                id: 'my-stack',
+            }
+        } as unknown as Stack;
+
+        // WHEN/THEN
+        expect(() => {
+            mapToCustomResource(logicalId, cfnType, cfnProps, {}, stack);
+        }).toThrow('Synthesizer of stack my-stack does not support custom resources. It must inherit from PulumiSynthesizerBase.');
+    });
+
+    test('Not a CustomResource', () => {
+        // GIVEN
+        const cfnType = 'AWS::S3::Bucket';
+        const logicalId = 'My-resource';
+
+
+        const stack = {
+            synthesizer: new MockSynth("bucket", "prefix/"),
+            node: {
+                id: 'my-stack',
+            }
+        } as unknown as Stack;
+
+        // WHEN
+        const returnValue = mapToCustomResource(logicalId, cfnType, {}, {}, stack);
+        // THEN
+        expect(returnValue).not.toBeDefined();
+    });
+});
diff --git a/tests/converters/app-converter.test.ts b/tests/converters/app-converter.test.ts
index 60faf81..50afa27 100644
--- a/tests/converters/app-converter.test.ts
+++ b/tests/converters/app-converter.test.ts
@@ -1,13 +1,15 @@
 import { AppConverter, StackConverter } from '../../src/converters/app-converter';
 import * as native from '@pulumi/aws-native';
+import * as fs from 'fs';
 import * as path from 'path';
 import * as mockfs from 'mock-fs';
 import * as pulumi from '@pulumi/pulumi';
 import { BucketPolicy } from '@pulumi/aws-native/s3';
 import { createStackManifest } from '../utils';
-import { promiseOf, setMocks, MockAppComponent } from '../mocks';
-import { StackManifest } from '../../src/assembly';
+import { promiseOf, setMocks, MockAppComponent, MockSynth } from '../mocks';
+import { StackManifest, StackManifestProps } from '../../src/assembly';
 import { MockResourceArgs } from '@pulumi/pulumi/runtime';
+import { Stack as CdkStack } from 'aws-cdk-lib/core';
 
 let resources: MockResourceArgs[] = [];
 beforeAll(() => {
@@ -441,6 +443,36 @@ describe('Stack Converter', () => {
         const cidrBlock2 = await promiseOf(subnet2.ipv6CidrBlock);
         expect(cidrBlock2).toEqual('cidr_ipv6AddressAttribute_2');
     });
+
+    test('can convert with custom resources', async () => {
+        const stackManifestPath = path.join(__dirname, '../test-data/custom-resource-stack/stack-manifest.json');
+        const props: StackManifestProps = JSON.parse(fs.readFileSync(stackManifestPath, 'utf-8'));
+        const manifest = new StackManifest(props);
+        const app = new MockAppComponent('/tmp/foo/bar/does/not/exist')
+        const stagingBucket = "my-bucket";
+        const customResourcePrefix = "my-prefix";
+        app.stacks[manifest.id] = {
+            synthesizer: new MockSynth(stagingBucket, customResourcePrefix),
+            node: {
+                id: 'my-stack',
+            }
+        } as unknown as CdkStack
+
+        const converter = new StackConverter(app, manifest);
+        converter.convert(new Set());
+
+        const customResource = converter.resources.get('DeployWebsiteCustomResourceD116527B');
+        expect(customResource).toBeDefined();
+
+        const customResourceEmulator = customResource?.resource! as native.cloudformation.CustomResourceEmulator;
+        expect(customResourceEmulator.bucket).toBeDefined();
+        expect(customResourceEmulator.data).toBeDefined();
+        expect(customResourceEmulator.serviceToken).toBeDefined();
+
+        // This uses GetAtt to get the destination bucket from the custom resource
+        const customResourceRole = converter.resources.get('CustomResourceRoleAB1EF463');
+        expect(customResourceRole).toBeDefined();
+    });
 });
 
 function createUrn(resource: string, logicalId: string): string {
diff --git a/tests/graph.test.ts b/tests/graph.test.ts
index 199b09c..da7e253 100644
--- a/tests/graph.test.ts
+++ b/tests/graph.test.ts
@@ -13,8 +13,10 @@
 // limitations under the License.
 
 import { GraphBuilder, GraphNode } from '../src/graph';
-import { StackManifest } from '../src/assembly';
+import { StackManifest, StackManifestProps } from '../src/assembly';
 import { createStackManifest } from './utils';
+import * as fs from 'fs';
+import * as path from 'path';
 
 describe('GraphBuilder', () => {
     const nodes = GraphBuilder.build(
@@ -474,6 +476,33 @@ test('pulumi resource type name fallsback when fqn not available', () => {
     expect(nodes[4].construct.type).toEqual('BucketPolicy');
 });
 
+test('parses custom resources', () => {
+    const stackManifestPath = path.join(__dirname, 'test-data/custom-resource-stack/stack-manifest.json');
+    const props: StackManifestProps = JSON.parse(fs.readFileSync(stackManifestPath, 'utf-8'));
+    const stackManifest = new StackManifest(props);
+    const graph = GraphBuilder.build(stackManifest);
+
+    const deployWebsiteCR = graph.nodes.find((node) => node.logicalId === 'DeployWebsiteCustomResourceD116527B');
+    expect(deployWebsiteCR).toBeDefined();
+    expect(deployWebsiteCR?.construct.type).toEqual('aws-cdk-lib:CfnResource');
+    expect(deployWebsiteCR?.resource).toBeDefined();
+    const deployWebsiteCRResource = deployWebsiteCR?.resource!;
+    expect(deployWebsiteCRResource.Type).toEqual('Custom::CDKBucketDeployment');
+    expect(deployWebsiteCRResource.Properties.ServiceToken).toEqual({ 'Fn::GetAtt': ['CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', 'Arn'] });
+    expect(deployWebsiteCRResource.Properties.SourceBucketNames).toEqual(['pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2']);
+    expect(deployWebsiteCRResource.Properties.SourceObjectKeys).toEqual(['a386ba9b8c0d9b386083b2f6952db278a5a0ce88f497484eb5e62172219468fd.zip']);
+
+    const testRole = graph.nodes.find((node) => node.logicalId === 'CustomResourceRoleAB1EF463');
+    expect(testRole).toBeDefined();
+    const policies = testRole?.resource?.Properties?.Policies;
+    expect(policies).toBeDefined();
+    expect(policies).toHaveLength(1);
+    const statement = policies[0].PolicyDocument?.Statement;
+    expect(statement).toBeDefined();
+    expect(statement).toHaveLength(1);
+    expect(statement[0].Resource).toEqual({ 'Fn::Join': ['', [{ 'Fn::GetAtt': ['DeployWebsiteCustomResourceD116527B', 'DestinationBucketArn'] }, '/*']] });
+});
+
 function edgesToArray(edges: Set<GraphNode>): string[] {
     return Array.from(edges).flatMap((value) => value.construct.path);
 }
diff --git a/tests/mocks.ts b/tests/mocks.ts
index 5c095c5..802867e 100644
--- a/tests/mocks.ts
+++ b/tests/mocks.ts
@@ -1,10 +1,11 @@
 import * as pulumi from '@pulumi/pulumi';
 import { CdkConstruct } from '../src/interop';
-import { Stack as CdkStack } from 'aws-cdk-lib/core';
+import { Stack as CdkStack, DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource, ISynthesisSession } from 'aws-cdk-lib/core';
 import { AppComponent, AppOptions } from '../src/types';
 import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime';
 import { Construct } from 'constructs';
 import { App, Stack } from '../src/stack';
+import { PulumiSynthesizerBase } from '../src/synthesizer';
 
 // Convert a pulumi.Output to a promise of the same type.
 export function promiseOf<T>(output: pulumi.Output<T>): Promise<T> {
@@ -142,6 +143,18 @@ export function setMocks(resources?: MockResourceArgs[]) {
                             repositoryUrl: '12345678910.dkr.ecr.us-east-1.amazonaws.com/' + args.inputs.name,
                         },
                     };
+                case 'aws-native:cloudformation:CustomResourceEmulator':
+                    resources?.push(args);
+                    return {
+                        id: args.inputs.logicalId + '_id',
+                        state: {
+                            ...args.inputs,
+                            id: args.inputs.logicalId + '_id',
+                            data: {
+                                "DestinationBucketArn": `arn:aws:s3:::${args.inputs.bucketName}`
+                            }
+                        },
+                    };
                 default:
                     resources?.push(args);
                     return {
@@ -158,3 +171,28 @@ export function setMocks(resources?: MockResourceArgs[]) {
 
     pulumi.runtime.setMocks(mocks, 'project', 'stack', false);
 }
+
+export class MockSynth extends PulumiSynthesizerBase {
+
+    constructor(readonly bucket: string, readonly prefix: string) {
+        super();
+    }
+    public stagingStack: CdkConstruct;
+    public getStagingBucket(): pulumi.Input<string> {
+        return this.bucket;
+    }
+    public getDeployTimePrefix(): string {
+        return this.prefix;
+    }
+
+    addFileAsset(asset: FileAssetSource): FileAssetLocation {
+        throw new Error('Method not implemented.');
+    }
+    addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation {
+        throw new Error('Method not implemented.');
+    }
+    synthesize(session: ISynthesisSession): void {
+        throw new Error('Method not implemented.');
+    }
+}
+
diff --git a/tests/test-data/custom-resource-stack/stack-manifest.json b/tests/test-data/custom-resource-stack/stack-manifest.json
new file mode 100644
index 0000000..69bbf0b
--- /dev/null
+++ b/tests/test-data/custom-resource-stack/stack-manifest.json
@@ -0,0 +1,1116 @@
+{
+    "id": "stack",
+    "templatePath": "test/stack",
+    "metadata": {
+        "s3deployment/WebsiteBucket/Resource": "WebsiteBucket75C24D94",
+        "s3deployment/WebsiteBucket/Policy/Resource": "WebsiteBucketPolicyE10E3262",
+        "s3deployment/WebsiteBucket/AutoDeleteObjectsCustomResource/Default": "WebsiteBucketAutoDeleteObjectsCustomResource8750E461",
+        "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
+        "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
+        "s3deployment/DeployWebsite/AwsCliLayer/Resource": "DeployWebsiteAwsCliLayer17DBC421",
+        "s3deployment/DeployWebsite/CustomResource/Default": "DeployWebsiteCustomResourceD116527B",
+        "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265",
+        "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
+        "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536",
+        "s3deployment/CustomResourceRole/Resource": "CustomResourceRoleAB1EF463"
+    },
+    "tree": {
+        "id": "stack",
+        "path": "",
+        "children": {
+            "s3deployment": {
+                "id": "s3deployment",
+                "path": "s3deployment",
+                "children": {
+                    "WebsiteBucket": {
+                        "id": "WebsiteBucket",
+                        "path": "s3deployment/WebsiteBucket",
+                        "children": {
+                            "Resource": {
+                                "id": "Resource",
+                                "path": "s3deployment/WebsiteBucket/Resource",
+                                "attributes": {
+                                    "aws:cdk:cloudformation:type": "AWS::S3::Bucket",
+                                    "aws:cdk:cloudformation:props": {
+                                        "publicAccessBlockConfiguration": {
+                                            "blockPublicAcls": false,
+                                            "blockPublicPolicy": false,
+                                            "ignorePublicAcls": false,
+                                            "restrictPublicBuckets": false
+                                        },
+                                        "tags": [
+                                            {
+                                                "key": "aws-cdk:auto-delete-objects",
+                                                "value": "true"
+                                            },
+                                            {
+                                                "key": "aws-cdk:cr-owned:5c1ba683",
+                                                "value": "true"
+                                            }
+                                        ],
+                                        "websiteConfiguration": {
+                                            "indexDocument": "index.html"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_s3.CfnBucket",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Policy": {
+                                "id": "Policy",
+                                "path": "s3deployment/WebsiteBucket/Policy",
+                                "children": {
+                                    "Resource": {
+                                        "id": "Resource",
+                                        "path": "s3deployment/WebsiteBucket/Policy/Resource",
+                                        "attributes": {
+                                            "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy",
+                                            "aws:cdk:cloudformation:props": {
+                                                "bucket": {
+                                                    "Ref": "WebsiteBucket75C24D94"
+                                                },
+                                                "policyDocument": {
+                                                    "Statement": [
+                                                        {
+                                                            "Action": "s3:GetObject",
+                                                            "Effect": "Allow",
+                                                            "Principal": {
+                                                                "AWS": "*"
+                                                            },
+                                                            "Resource": {
+                                                                "Fn::Join": [
+                                                                    "",
+                                                                    [
+                                                                        {
+                                                                            "Fn::GetAtt": [
+                                                                                "WebsiteBucket75C24D94",
+                                                                                "Arn"
+                                                                            ]
+                                                                        },
+                                                                        "/*"
+                                                                    ]
+                                                                ]
+                                                            }
+                                                        },
+                                                        {
+                                                            "Action": [
+                                                                "s3:PutBucketPolicy",
+                                                                "s3:GetBucket*",
+                                                                "s3:List*",
+                                                                "s3:DeleteObject*"
+                                                            ],
+                                                            "Effect": "Allow",
+                                                            "Principal": {
+                                                                "AWS": {
+                                                                    "Fn::GetAtt": [
+                                                                        "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
+                                                                        "Arn"
+                                                                    ]
+                                                                }
+                                                            },
+                                                            "Resource": [
+                                                                {
+                                                                    "Fn::GetAtt": [
+                                                                        "WebsiteBucket75C24D94",
+                                                                        "Arn"
+                                                                    ]
+                                                                },
+                                                                {
+                                                                    "Fn::Join": [
+                                                                        "",
+                                                                        [
+                                                                            {
+                                                                                "Fn::GetAtt": [
+                                                                                    "WebsiteBucket75C24D94",
+                                                                                    "Arn"
+                                                                                ]
+                                                                            },
+                                                                            "/*"
+                                                                        ]
+                                                                    ]
+                                                                }
+                                                            ]
+                                                        }
+                                                    ],
+                                                    "Version": "2012-10-17"
+                                                }
+                                            }
+                                        },
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_s3.BucketPolicy",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "AutoDeleteObjectsCustomResource": {
+                                "id": "AutoDeleteObjectsCustomResource",
+                                "path": "s3deployment/WebsiteBucket/AutoDeleteObjectsCustomResource",
+                                "children": {
+                                    "Default": {
+                                        "id": "Default",
+                                        "path": "s3deployment/WebsiteBucket/AutoDeleteObjectsCustomResource/Default",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.CfnResource",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.CustomResource",
+                                    "version": "2.149.0"
+                                }
+                            }
+                        },
+                        "constructInfo": {
+                            "fqn": "aws-cdk-lib.aws_s3.Bucket",
+                            "version": "2.149.0"
+                        }
+                    },
+                    "Custom::S3AutoDeleteObjectsCustomResourceProvider": {
+                        "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider",
+                        "path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider",
+                        "children": {
+                            "Staging": {
+                                "id": "Staging",
+                                "path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.AssetStaging",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Role": {
+                                "id": "Role",
+                                "path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.CfnResource",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Handler": {
+                                "id": "Handler",
+                                "path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.CfnResource",
+                                    "version": "2.149.0"
+                                }
+                            }
+                        },
+                        "constructInfo": {
+                            "fqn": "aws-cdk-lib.CustomResourceProviderBase",
+                            "version": "2.149.0"
+                        }
+                    },
+                    "DeployWebsite": {
+                        "id": "DeployWebsite",
+                        "path": "s3deployment/DeployWebsite",
+                        "children": {
+                            "AwsCliLayer": {
+                                "id": "AwsCliLayer",
+                                "path": "s3deployment/DeployWebsite/AwsCliLayer",
+                                "children": {
+                                    "Code": {
+                                        "id": "Code",
+                                        "path": "s3deployment/DeployWebsite/AwsCliLayer/Code",
+                                        "children": {
+                                            "Stage": {
+                                                "id": "Stage",
+                                                "path": "s3deployment/DeployWebsite/AwsCliLayer/Code/Stage",
+                                                "constructInfo": {
+                                                    "fqn": "aws-cdk-lib.AssetStaging",
+                                                    "version": "2.149.0"
+                                                }
+                                            },
+                                            "AssetBucket": {
+                                                "id": "AssetBucket",
+                                                "path": "s3deployment/DeployWebsite/AwsCliLayer/Code/AssetBucket",
+                                                "constructInfo": {
+                                                    "fqn": "aws-cdk-lib.aws_s3.BucketBase",
+                                                    "version": "2.149.0"
+                                                }
+                                            }
+                                        },
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_s3_assets.Asset",
+                                            "version": "2.149.0"
+                                        }
+                                    },
+                                    "Resource": {
+                                        "id": "Resource",
+                                        "path": "s3deployment/DeployWebsite/AwsCliLayer/Resource",
+                                        "attributes": {
+                                            "aws:cdk:cloudformation:type": "AWS::Lambda::LayerVersion",
+                                            "aws:cdk:cloudformation:props": {
+                                                "content": {
+                                                    "s3Bucket": "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2",
+                                                    "s3Key": "deploy-time/2e978c20dab148afad7b0a7cbe53b42a2bcd72c40e670a644721752fae05c2ca.zip"
+                                                },
+                                                "description": "/opt/awscli/aws"
+                                            }
+                                        },
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_lambda.CfnLayerVersion",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.lambda_layer_awscli.AwsCliLayer",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "CustomResourceHandler": {
+                                "id": "CustomResourceHandler",
+                                "path": "s3deployment/DeployWebsite/CustomResourceHandler",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_lambda.SingletonFunction",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Asset1": {
+                                "id": "Asset1",
+                                "path": "s3deployment/DeployWebsite/Asset1",
+                                "children": {
+                                    "Stage": {
+                                        "id": "Stage",
+                                        "path": "s3deployment/DeployWebsite/Asset1/Stage",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.AssetStaging",
+                                            "version": "2.149.0"
+                                        }
+                                    },
+                                    "AssetBucket": {
+                                        "id": "AssetBucket",
+                                        "path": "s3deployment/DeployWebsite/Asset1/AssetBucket",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_s3.BucketBase",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_s3_assets.Asset",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "CustomResource": {
+                                "id": "CustomResource",
+                                "path": "s3deployment/DeployWebsite/CustomResource",
+                                "children": {
+                                    "Default": {
+                                        "id": "Default",
+                                        "path": "s3deployment/DeployWebsite/CustomResource/Default",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.CfnResource",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.CustomResource",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "DestinationBucket": {
+                                "id": "DestinationBucket",
+                                "path": "s3deployment/DeployWebsite/DestinationBucket",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_s3.BucketBase",
+                                    "version": "2.149.0"
+                                }
+                            }
+                        },
+                        "constructInfo": {
+                            "fqn": "aws-cdk-lib.aws_s3_deployment.BucketDeployment",
+                            "version": "2.149.0"
+                        }
+                    },
+                    "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C": {
+                        "id": "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C",
+                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C",
+                        "children": {
+                            "ServiceRole": {
+                                "id": "ServiceRole",
+                                "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole",
+                                "children": {
+                                    "ImportServiceRole": {
+                                        "id": "ImportServiceRole",
+                                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/ImportServiceRole",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.Resource",
+                                            "version": "2.149.0"
+                                        }
+                                    },
+                                    "Resource": {
+                                        "id": "Resource",
+                                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource",
+                                        "attributes": {
+                                            "aws:cdk:cloudformation:type": "AWS::IAM::Role",
+                                            "aws:cdk:cloudformation:props": {
+                                                "assumeRolePolicyDocument": {
+                                                    "Statement": [
+                                                        {
+                                                            "Action": "sts:AssumeRole",
+                                                            "Effect": "Allow",
+                                                            "Principal": {
+                                                                "Service": "lambda.amazonaws.com"
+                                                            }
+                                                        }
+                                                    ],
+                                                    "Version": "2012-10-17"
+                                                },
+                                                "managedPolicyArns": [
+                                                    {
+                                                        "Fn::Join": [
+                                                            "",
+                                                            [
+                                                                "arn:",
+                                                                {
+                                                                    "Ref": "AWS::Partition"
+                                                                },
+                                                                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+                                                            ]
+                                                        ]
+                                                    }
+                                                ]
+                                            }
+                                        },
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_iam.CfnRole",
+                                            "version": "2.149.0"
+                                        }
+                                    },
+                                    "DefaultPolicy": {
+                                        "id": "DefaultPolicy",
+                                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy",
+                                        "children": {
+                                            "Resource": {
+                                                "id": "Resource",
+                                                "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource",
+                                                "attributes": {
+                                                    "aws:cdk:cloudformation:type": "AWS::IAM::Policy",
+                                                    "aws:cdk:cloudformation:props": {
+                                                        "policyDocument": {
+                                                            "Statement": [
+                                                                {
+                                                                    "Action": [
+                                                                        "s3:GetObject*",
+                                                                        "s3:GetBucket*",
+                                                                        "s3:List*"
+                                                                    ],
+                                                                    "Effect": "Allow",
+                                                                    "Resource": [
+                                                                        {
+                                                                            "Fn::Join": [
+                                                                                "",
+                                                                                [
+                                                                                    "arn:",
+                                                                                    {
+                                                                                        "Ref": "AWS::Partition"
+                                                                                    },
+                                                                                    ":s3:::pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2"
+                                                                                ]
+                                                                            ]
+                                                                        },
+                                                                        {
+                                                                            "Fn::Join": [
+                                                                                "",
+                                                                                [
+                                                                                    "arn:",
+                                                                                    {
+                                                                                        "Ref": "AWS::Partition"
+                                                                                    },
+                                                                                    ":s3:::pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2/*"
+                                                                                ]
+                                                                            ]
+                                                                        }
+                                                                    ]
+                                                                },
+                                                                {
+                                                                    "Action": [
+                                                                        "s3:GetObject*",
+                                                                        "s3:GetBucket*",
+                                                                        "s3:List*",
+                                                                        "s3:DeleteObject*",
+                                                                        "s3:PutObject",
+                                                                        "s3:PutObjectLegalHold",
+                                                                        "s3:PutObjectRetention",
+                                                                        "s3:PutObjectTagging",
+                                                                        "s3:PutObjectVersionTagging",
+                                                                        "s3:Abort*"
+                                                                    ],
+                                                                    "Effect": "Allow",
+                                                                    "Resource": [
+                                                                        {
+                                                                            "Fn::GetAtt": [
+                                                                                "WebsiteBucket75C24D94",
+                                                                                "Arn"
+                                                                            ]
+                                                                        },
+                                                                        {
+                                                                            "Fn::Join": [
+                                                                                "",
+                                                                                [
+                                                                                    {
+                                                                                        "Fn::GetAtt": [
+                                                                                            "WebsiteBucket75C24D94",
+                                                                                            "Arn"
+                                                                                        ]
+                                                                                    },
+                                                                                    "/*"
+                                                                                ]
+                                                                            ]
+                                                                        }
+                                                                    ]
+                                                                }
+                                                            ],
+                                                            "Version": "2012-10-17"
+                                                        },
+                                                        "policyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
+                                                        "roles": [
+                                                            {
+                                                                "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
+                                                            }
+                                                        ]
+                                                    }
+                                                },
+                                                "constructInfo": {
+                                                    "fqn": "aws-cdk-lib.aws_iam.CfnPolicy",
+                                                    "version": "2.149.0"
+                                                }
+                                            }
+                                        },
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_iam.Policy",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_iam.Role",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Code": {
+                                "id": "Code",
+                                "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code",
+                                "children": {
+                                    "Stage": {
+                                        "id": "Stage",
+                                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/Stage",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.AssetStaging",
+                                            "version": "2.149.0"
+                                        }
+                                    },
+                                    "AssetBucket": {
+                                        "id": "AssetBucket",
+                                        "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/AssetBucket",
+                                        "constructInfo": {
+                                            "fqn": "aws-cdk-lib.aws_s3.BucketBase",
+                                            "version": "2.149.0"
+                                        }
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_s3_assets.Asset",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Resource": {
+                                "id": "Resource",
+                                "path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource",
+                                "attributes": {
+                                    "aws:cdk:cloudformation:type": "AWS::Lambda::Function",
+                                    "aws:cdk:cloudformation:props": {
+                                        "code": {
+                                            "s3Bucket": "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2",
+                                            "s3Key": "deploy-time/2d56e153cac88d3e0c2f842e8e6f6783b8725bf91f95e0673b4725448a56e96d.zip"
+                                        },
+                                        "environment": {
+                                            "variables": {
+                                                "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
+                                            }
+                                        },
+                                        "handler": "index.handler",
+                                        "layers": [
+                                            {
+                                                "Ref": "DeployWebsiteAwsCliLayer17DBC421"
+                                            }
+                                        ],
+                                        "role": {
+                                            "Fn::GetAtt": [
+                                                "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265",
+                                                "Arn"
+                                            ]
+                                        },
+                                        "runtime": "python3.9",
+                                        "timeout": 900
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_lambda.CfnFunction",
+                                    "version": "2.149.0"
+                                }
+                            }
+                        },
+                        "constructInfo": {
+                            "fqn": "aws-cdk-lib.aws_lambda.Function",
+                            "version": "2.149.0"
+                        }
+                    },
+                    "CustomResourceRole": {
+                        "id": "CustomResourceRole",
+                        "path": "s3deployment/CustomResourceRole",
+                        "children": {
+                            "ImportCustomResourceRole": {
+                                "id": "ImportCustomResourceRole",
+                                "path": "s3deployment/CustomResourceRole/ImportCustomResourceRole",
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.Resource",
+                                    "version": "2.149.0"
+                                }
+                            },
+                            "Resource": {
+                                "id": "Resource",
+                                "path": "s3deployment/CustomResourceRole/Resource",
+                                "attributes": {
+                                    "aws:cdk:cloudformation:type": "AWS::IAM::Role",
+                                    "aws:cdk:cloudformation:props": {
+                                        "assumeRolePolicyDocument": {
+                                            "Statement": [
+                                                {
+                                                    "Action": "sts:AssumeRole",
+                                                    "Effect": "Allow",
+                                                    "Principal": {
+                                                        "Service": "lambda.amazonaws.com"
+                                                    }
+                                                }
+                                            ],
+                                            "Version": "2012-10-17"
+                                        },
+                                        "policies": [
+                                            {
+                                                "policyName": "CustomResourcePolicy",
+                                                "policyDocument": {
+                                                    "Statement": [
+                                                        {
+                                                            "Action": "s3:GetObject",
+                                                            "Effect": "Allow",
+                                                            "Resource": {
+                                                                "Fn::Join": [
+                                                                    "",
+                                                                    [
+                                                                        {
+                                                                            "Fn::GetAtt": [
+                                                                                "DeployWebsiteCustomResourceD116527B",
+                                                                                "DestinationBucketArn"
+                                                                            ]
+                                                                        },
+                                                                        "/*"
+                                                                    ]
+                                                                ]
+                                                            }
+                                                        }
+                                                    ],
+                                                    "Version": "2012-10-17"
+                                                }
+                                            }
+                                        ]
+                                    }
+                                },
+                                "constructInfo": {
+                                    "fqn": "aws-cdk-lib.aws_iam.CfnRole",
+                                    "version": "2.149.0"
+                                }
+                            }
+                        },
+                        "constructInfo": {
+                            "fqn": "aws-cdk-lib.aws_iam.Role",
+                            "version": "2.149.0"
+                        }
+                    }
+                },
+                "constructInfo": {
+                    "fqn": "aws-cdk-lib.Stack",
+                    "version": "2.156.0"
+                }
+            },
+            "Tree": {
+                "id": "Tree",
+                "path": "Tree",
+                "constructInfo": {
+                    "fqn": "constructs.Construct",
+                    "version": "10.3.0"
+                }
+            }
+        },
+        "constructInfo": {
+            "fqn": "aws-cdk-lib.App",
+            "version": "2.156.0"
+        }
+    },
+    "template": {
+        "Resources": {
+            "WebsiteBucket75C24D94": {
+                "Type": "AWS::S3::Bucket",
+                "Properties": {
+                    "PublicAccessBlockConfiguration": {
+                        "BlockPublicAcls": false,
+                        "BlockPublicPolicy": false,
+                        "IgnorePublicAcls": false,
+                        "RestrictPublicBuckets": false
+                    },
+                    "Tags": [
+                        {
+                            "Key": "aws-cdk:auto-delete-objects",
+                            "Value": "true"
+                        },
+                        {
+                            "Key": "aws-cdk:cr-owned:5c1ba683",
+                            "Value": "true"
+                        }
+                    ],
+                    "WebsiteConfiguration": {
+                        "IndexDocument": "index.html"
+                    }
+                },
+                "UpdateReplacePolicy": "Delete",
+                "DeletionPolicy": "Delete",
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/WebsiteBucket/Resource"
+                }
+            },
+            "WebsiteBucketPolicyE10E3262": {
+                "Type": "AWS::S3::BucketPolicy",
+                "Properties": {
+                    "Bucket": {
+                        "Ref": "WebsiteBucket75C24D94"
+                    },
+                    "PolicyDocument": {
+                        "Statement": [
+                            {
+                                "Action": "s3:GetObject",
+                                "Effect": "Allow",
+                                "Principal": {
+                                    "AWS": "*"
+                                },
+                                "Resource": {
+                                    "Fn::Join": [
+                                        "",
+                                        [
+                                            {
+                                                "Fn::GetAtt": [
+                                                    "WebsiteBucket75C24D94",
+                                                    "Arn"
+                                                ]
+                                            },
+                                            "/*"
+                                        ]
+                                    ]
+                                }
+                            },
+                            {
+                                "Action": [
+                                    "s3:PutBucketPolicy",
+                                    "s3:GetBucket*",
+                                    "s3:List*",
+                                    "s3:DeleteObject*"
+                                ],
+                                "Effect": "Allow",
+                                "Principal": {
+                                    "AWS": {
+                                        "Fn::GetAtt": [
+                                            "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
+                                            "Arn"
+                                        ]
+                                    }
+                                },
+                                "Resource": [
+                                    {
+                                        "Fn::GetAtt": [
+                                            "WebsiteBucket75C24D94",
+                                            "Arn"
+                                        ]
+                                    },
+                                    {
+                                        "Fn::Join": [
+                                            "",
+                                            [
+                                                {
+                                                    "Fn::GetAtt": [
+                                                        "WebsiteBucket75C24D94",
+                                                        "Arn"
+                                                    ]
+                                                },
+                                                "/*"
+                                            ]
+                                        ]
+                                    }
+                                ]
+                            }
+                        ],
+                        "Version": "2012-10-17"
+                    }
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/WebsiteBucket/Policy/Resource"
+                }
+            },
+            "WebsiteBucketAutoDeleteObjectsCustomResource8750E461": {
+                "Type": "Custom::S3AutoDeleteObjects",
+                "Properties": {
+                    "ServiceToken": {
+                        "Fn::GetAtt": [
+                            "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
+                            "Arn"
+                        ]
+                    },
+                    "BucketName": {
+                        "Ref": "WebsiteBucket75C24D94"
+                    }
+                },
+                "DependsOn": [
+                    "WebsiteBucketPolicyE10E3262"
+                ],
+                "UpdateReplacePolicy": "Delete",
+                "DeletionPolicy": "Delete",
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/WebsiteBucket/AutoDeleteObjectsCustomResource/Default"
+                }
+            },
+            "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
+                "Type": "AWS::IAM::Role",
+                "Properties": {
+                    "AssumeRolePolicyDocument": {
+                        "Version": "2012-10-17",
+                        "Statement": [
+                            {
+                                "Action": "sts:AssumeRole",
+                                "Effect": "Allow",
+                                "Principal": {
+                                    "Service": "lambda.amazonaws.com"
+                                }
+                            }
+                        ]
+                    },
+                    "ManagedPolicyArns": [
+                        {
+                            "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+                        }
+                    ]
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role"
+                }
+            },
+            "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
+                "Type": "AWS::Lambda::Function",
+                "Properties": {
+                    "Code": {
+                        "S3Bucket": "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2",
+                        "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip"
+                    },
+                    "Timeout": 900,
+                    "MemorySize": 128,
+                    "Handler": "index.handler",
+                    "Role": {
+                        "Fn::GetAtt": [
+                            "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
+                            "Arn"
+                        ]
+                    },
+                    "Runtime": "nodejs20.x",
+                    "Description": {
+                        "Fn::Join": [
+                            "",
+                            [
+                                "Lambda function for auto-deleting objects in ",
+                                {
+                                    "Ref": "WebsiteBucket75C24D94"
+                                },
+                                " S3 bucket."
+                            ]
+                        ]
+                    }
+                },
+                "DependsOn": [
+                    "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092"
+                ],
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler",
+                    "aws:asset:path": "asset.faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6",
+                    "aws:asset:property": "Code"
+                }
+            },
+            "DeployWebsiteAwsCliLayer17DBC421": {
+                "Type": "AWS::Lambda::LayerVersion",
+                "Properties": {
+                    "Content": {
+                        "S3Bucket": "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2",
+                        "S3Key": "deploy-time/2e978c20dab148afad7b0a7cbe53b42a2bcd72c40e670a644721752fae05c2ca.zip"
+                    },
+                    "Description": "/opt/awscli/aws"
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/DeployWebsite/AwsCliLayer/Resource",
+                    "aws:asset:path": "asset.2e978c20dab148afad7b0a7cbe53b42a2bcd72c40e670a644721752fae05c2ca.zip",
+                    "aws:asset:is-bundled": false,
+                    "aws:asset:property": "Content"
+                }
+            },
+            "DeployWebsiteCustomResourceD116527B": {
+                "Type": "Custom::CDKBucketDeployment",
+                "Properties": {
+                    "ServiceToken": {
+                        "Fn::GetAtt": [
+                            "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536",
+                            "Arn"
+                        ]
+                    },
+                    "SourceBucketNames": [
+                        "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2"
+                    ],
+                    "SourceObjectKeys": [
+                        "a386ba9b8c0d9b386083b2f6952db278a5a0ce88f497484eb5e62172219468fd.zip"
+                    ],
+                    "SourceMarkers": [
+                        {}
+                    ],
+                    "DestinationBucketName": {
+                        "Ref": "WebsiteBucket75C24D94"
+                    },
+                    "Prune": true,
+                    "DestinationBucketArn": {
+                        "Fn::GetAtt": [
+                            "WebsiteBucket75C24D94",
+                            "Arn"
+                        ]
+                    }
+                },
+                "UpdateReplacePolicy": "Delete",
+                "DeletionPolicy": "Delete",
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/DeployWebsite/CustomResource/Default"
+                }
+            },
+            "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": {
+                "Type": "AWS::IAM::Role",
+                "Properties": {
+                    "AssumeRolePolicyDocument": {
+                        "Statement": [
+                            {
+                                "Action": "sts:AssumeRole",
+                                "Effect": "Allow",
+                                "Principal": {
+                                    "Service": "lambda.amazonaws.com"
+                                }
+                            }
+                        ],
+                        "Version": "2012-10-17"
+                    },
+                    "ManagedPolicyArns": [
+                        {
+                            "Fn::Join": [
+                                "",
+                                [
+                                    "arn:",
+                                    {
+                                        "Ref": "AWS::Partition"
+                                    },
+                                    ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+                                ]
+                            ]
+                        }
+                    ]
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource"
+                }
+            },
+            "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": {
+                "Type": "AWS::IAM::Policy",
+                "Properties": {
+                    "PolicyDocument": {
+                        "Statement": [
+                            {
+                                "Action": [
+                                    "s3:GetObject*",
+                                    "s3:GetBucket*",
+                                    "s3:List*"
+                                ],
+                                "Effect": "Allow",
+                                "Resource": [
+                                    {
+                                        "Fn::Join": [
+                                            "",
+                                            [
+                                                "arn:",
+                                                {
+                                                    "Ref": "AWS::Partition"
+                                                },
+                                                ":s3:::pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2"
+                                            ]
+                                        ]
+                                    },
+                                    {
+                                        "Fn::Join": [
+                                            "",
+                                            [
+                                                "arn:",
+                                                {
+                                                    "Ref": "AWS::Partition"
+                                                },
+                                                ":s3:::pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2/*"
+                                            ]
+                                        ]
+                                    }
+                                ]
+                            },
+                            {
+                                "Action": [
+                                    "s3:GetObject*",
+                                    "s3:GetBucket*",
+                                    "s3:List*",
+                                    "s3:DeleteObject*",
+                                    "s3:PutObject",
+                                    "s3:PutObjectLegalHold",
+                                    "s3:PutObjectRetention",
+                                    "s3:PutObjectTagging",
+                                    "s3:PutObjectVersionTagging",
+                                    "s3:Abort*"
+                                ],
+                                "Effect": "Allow",
+                                "Resource": [
+                                    {
+                                        "Fn::GetAtt": [
+                                            "WebsiteBucket75C24D94",
+                                            "Arn"
+                                        ]
+                                    },
+                                    {
+                                        "Fn::Join": [
+                                            "",
+                                            [
+                                                {
+                                                    "Fn::GetAtt": [
+                                                        "WebsiteBucket75C24D94",
+                                                        "Arn"
+                                                    ]
+                                                },
+                                                "/*"
+                                            ]
+                                        ]
+                                    }
+                                ]
+                            }
+                        ],
+                        "Version": "2012-10-17"
+                    },
+                    "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
+                    "Roles": [
+                        {
+                            "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
+                        }
+                    ]
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource"
+                }
+            },
+            "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": {
+                "Type": "AWS::Lambda::Function",
+                "Properties": {
+                    "Code": {
+                        "S3Bucket": "pulumi-cdk-stom-res-d817419f-staging-616138583583-us-west-2",
+                        "S3Key": "deploy-time/2d56e153cac88d3e0c2f842e8e6f6783b8725bf91f95e0673b4725448a56e96d.zip"
+                    },
+                    "Environment": {
+                        "Variables": {
+                            "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
+                        }
+                    },
+                    "Handler": "index.handler",
+                    "Layers": [
+                        {
+                            "Ref": "DeployWebsiteAwsCliLayer17DBC421"
+                        }
+                    ],
+                    "Role": {
+                        "Fn::GetAtt": [
+                            "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265",
+                            "Arn"
+                        ]
+                    },
+                    "Runtime": "python3.9",
+                    "Timeout": 900
+                },
+                "DependsOn": [
+                    "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
+                    "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
+                ],
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource",
+                    "aws:asset:path": "asset.2d56e153cac88d3e0c2f842e8e6f6783b8725bf91f95e0673b4725448a56e96d",
+                    "aws:asset:is-bundled": false,
+                    "aws:asset:property": "Code"
+                }
+            },
+            "CustomResourceRoleAB1EF463": {
+                "Type": "AWS::IAM::Role",
+                "Properties": {
+                    "AssumeRolePolicyDocument": {
+                        "Statement": [
+                            {
+                                "Action": "sts:AssumeRole",
+                                "Effect": "Allow",
+                                "Principal": {
+                                    "Service": "lambda.amazonaws.com"
+                                }
+                            }
+                        ],
+                        "Version": "2012-10-17"
+                    },
+                    "Policies": [
+                        {
+                            "PolicyDocument": {
+                                "Statement": [
+                                    {
+                                        "Action": "s3:GetObject",
+                                        "Effect": "Allow",
+                                        "Resource": {
+                                            "Fn::Join": [
+                                                "",
+                                                [
+                                                    {
+                                                        "Fn::GetAtt": [
+                                                            "DeployWebsiteCustomResourceD116527B",
+                                                            "DestinationBucketArn"
+                                                        ]
+                                                    },
+                                                    "/*"
+                                                ]
+                                            ]
+                                        }
+                                    }
+                                ],
+                                "Version": "2012-10-17"
+                            },
+                            "PolicyName": "CustomResourcePolicy"
+                        }
+                    ]
+                },
+                "Metadata": {
+                    "aws:cdk:path": "s3deployment/CustomResourceRole/Resource"
+                }
+            }
+        }
+    },
+    "dependencies": []
+}
\ No newline at end of file