Repository: Sceptre/sceptre-cdk-handler (language: python)

Issue:
Making CDK release-ready (Part 2) comprehensive example project
**This is the second in a series of three pull requests.** The final one after this will be an overhaul of the Readme. The existing Readme is now out-of-date, but they will be rectified in the next PR (to keep this one smaller).

This PR introduces a comprehensive example project, demonstrating both the "bootstrapped" and the "bootstrapless" deployment_types, complete with modified bootstrap template and extensive commentary.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 659610c..431b455 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 ## What is this?
 
 `sceptre-cdk-handler` is a TemplateHandler for Sceptre (versions 2.7 and up) that lets you use a
-Python AWS CDK template as a stack's template.
+Python AWS CDK stack class as a stack's template.
 
 This template handler will use the AWS CDK to synthesize the CDK stack into a CloudFormation template
 and then run `npx cdk-assets` to publish any required assets to S3/ECR.
@@ -15,21 +15,208 @@ In other words, by using this handler with Sceptre, _you skip ever using `cdk de
 By using this handler, you can now use CDK templates with all your favorite Sceptre commands, like
 `launch`, `validate`, `generate`, and `diff` (along with all the rest)!
 
+## Why would you want to use CDK with Sceptre?
+**Aren't those two ways to do the same thing?**
+
+CDK and Sceptre really specialize in two different things, though there's definitely overlap.
+
+CDK's whole philosophy is that developers should be able to write code in a language they know and
+have that compile into an _artifact_ that defines cloud infrastructure resources. Of course, that
+"artifact" is ultimately a CloudFormation template. **CDK's specialty, thus, is really in _programmatic_
+template generation with sane defaults and constructs that simplify infrastructure configuration.** It
+hides a lot of the "magic" behind the scenes so developers can simply prescribe what they want. CDK
+also has deployment mechanisms to deploy that infrastructure, but they aren't really CDK's strength.
+
+90% of everything CDK does is at "compile-time", when the templates are being rendered. It has a very
+limited ability to execute custom code at deployment time, and never really _between_ the deployments
+of custom stacks and resources. Furthermore, its way of wiring together stacks uses CloudFormation
+exported outputs and imports, which are rather rigid and can have a host of unintended consequences,
+especially when values might change.
+
+In contrast, Sceptre is a _deployment orchestration_ tool. It excels in deploying entire environments
+of stacks, "wiring them together" using powerful (and easily customizable) resolvers, hooks, and
+template handlers. **Sceptre is "template agnostic"**, supporting YAML and JSON CloudFormation
+templates, even augmented with Jinja2 execution logic. Furthermore, out-of-the-box, Sceptre supports
+_any custom Python code to generate templates_, with the only requirement being that it needs to return
+a string.
+
+_So why use CDK and Sceptre together?_ Because CDK provides excellent template generation capabilities
+and Sceptre will gladly use those. Furthermore, Sceptre has the ability to easily wire together an
+entire environment _regardless of how that environment's CloudFormation templates are generated._ Thus,
+Sceptre will happily (and fairly intuitively) deploy (and "wire together") stacks developed in vanilla
+CloudFormation YAML/JSON, templates augmented with Jinja2, Troposphere-generated templates,
+AWS SAM templates, and CDK constructs. They can be deployed as a coherent environment that
+interoperates, deployed with insight into the various dependencies between stacks. Furthermore,
+using Sceptre's powerful hooks, you can execute customized pre-deployment and post-deployment code
+to prepare the way for or clean up after a given stack deployment. This is powerful and not something
+CDK provides a means to accomplish.
+
 ## How to install sceptre-cdk-handler
 
 1) Install the Sceptre CDK handler using `pip install sceptre-cdk-handler`
 2) Install the AWS CDK following the instructions in [AWS CDK Getting Started](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)
 3) Install the [CDK-Assets](https://www.npmjs.com/package/cdk-assets) NPM package using `npm install cdk-assets`
 
+If your project will require building and pushing docker image assets to ECR, you will also need to
+install docker and make it accessible on your PATH.
+
 ## How to use sceptre-cdk-handler
 
-The template "type" for this handler is `cdk`. 
+The template "type" for this handler is `cdk`.
+
+### Deployment Types
+The CDK Handler supports two different deployment types, which function somewhat differently. These
+are "bootstrapped" and "bootstrapless", both passed to the `deployment_type` template handler argument.
+
+#### The "bootstrapped" deployment_type
+The "bootstrapped" deployment type is a more typical CDK-like way to deploy infrastructure and
+conforms more to the standard ways _CDK_ operates. If your organization has a lot of CDK infrastructure,
+using this deployment_type likely will allow Sceptre to interoperate with existing patterns and
+policies.
+
+The `"bootstrapped"` deployment_type causes Sceptre assets (namely S3-destined files and
+ECR-destined images) to be deployed using the usual CDK-bootstrapped machinery. Specifically, by
+referencing a "qualifier", CDK looks for a corresponding stack in the AWS account that contains the
+specific S3 bucket and ECR repo, as well as IAM roles to be assumed in order to build and push those
+assets up to the cloud.
+
+In order to use the "bootstrapped" deployment type to push assets to the cloud, a CDK bootstrap stack
+with matching qualifier must already be deployed. It may be deployed via CDK (outside of Sceptre) or
+you can use CDK to generate the bootstrap template for Sceptre to deploy using
+`cdk bootstrap --show-template > cdk-bootstrap.yaml`. It is recommended, if not reusing an existing
+bootstrap stack, to deploy the bootstrap stack using Sceptre, as it will allow you to add outputs to
+the template and reference those when setting up your CDK-based StackConfigs.
+
+With that said, a bootstrap stack is not actually necessary if your stack includes no S3 or ECR
+assets to push.
+
+**Important:** See section below on IAM implications and behavior of using this handler and how it
+corresponds to the roles in the bootstrap stack.
+
+#### The "bootstrapless" deployment_type
+While the "bootstrapped" deployment_type is more similar to CDK's way of operating, it's less typical
+for Sceptre. Sceptre shines by allowing you to define your infrastructure however you want (in however
+many stacks you want) and then "wire them together" using powerful (and very handy) hooks and
+resolvers (like `!stack_output`). Thus, a more "Sceptre-like" way would be to avoid using the CDK
+bootstrap stack and just providing references to the required asset-related infrastructure with
+resolvers like `!stack_output`.
+
+The "bootstrapless" deployment_type uses the [cdk-bootstrapless-synthesizer](https://github.com/aws-samples/cdk-bootstrapless-synthesizer)
+to handle assets without needing a bootstrap stack. Instead, you can provide the relevant asset-related
+configurations as needed, pulling values from other stack outputs using resolvers.
+
+If you don't need to utilize a pre-existing bootstrap stack or don't need or want the overhead of
+having a bootstrap stack with all the infrastructure resources created along with that (many of which
+you might not actually need), the "bootstrapless" deployment_type is a simpler approach. However,
+it will require you to supply the needed bucket, ECR repository, and other configurations if you're
+deploying file or image assets. These will need to be supplied on the `bootstrapless_config` argument.
+
+**Why wouldn't you want to use the CDK Boostrap stack if it provides "everything you'd need"?**
+A CDK bootstrap stack has a lot of resources, defined all together in a single stack. It's
+auto-generated via CDK. As such, it has a lot of resources you probably might not actually need,
+depending on the sort of resources you have in your stack. For example, if you aren't actually
+building and pushing images to ECR, creating a new ECR repository (which is in every CDK bootstrap
+stack) isn't necessary.
+
+Also, every CDK bootstrap stack contains 4 different IAM roles used for four different actions
+(cloudformation service role, execution role, file asset pushing, image asset pushing). While it is
+possible to make Sceptre use all four of these roles for all four of these actions, it's not the
+normal way Sceptre operates (where it uses the same role/profile for the all deployment actions).
+For more information on this handler and IAM, see [the section on IAM](#IAM-and-authentication).
+
+If there is an existing bootstrap stack you need Sceptre to integrate
+with, using `deployment_type: "bootstrapped"` will adhere to those norms. But if you don't need to
+integrate with an existing CDK ecosystem and the rest of your infrastructure is deployed with
+Sceptre as well, using `deployment_type: "bootstrapless"`  will prove a simpler, more straight-forward
+way to deploy and integrate your CDK-stacks into the rest of your environment.
+
+### Making your stack class
+In order to properly support this handler's functionality, your Stack class on your Python file
+should subclass [`sceptre_cdk_handler.SceptreCdkStack`](sceptre_cdk_handler/cdk_builder.py).
+Furthermore, it should have this `__init__` signature and invoke the base class `__init__()` this
+way:
+
+```python
+import aws_cdk
+from sceptre_cdk_handler import SceptreCdkStack
+
+# Subclass SceptreCDKStack
+class CdkStack(SceptreCdkStack):
+  def __init__(self, scope: aws_cdk.App, id: str, sceptre_user_data: dict, **kwargs):
+    # Be sure you invoke super().__init__() and pass the **kwargs along to it
+    super().__init__(scope, id, sceptre_user_data, **kwargs)
+    # and then you can add your resources...
+```
+
+### Creating your StackConfig
+Here is a simple example of how to configure the template handler. For a more complete Sceptre
+Project configuration, with examples of both `bootstrapped` and `bootstrapless` configurations,
+see [the example directory in this repo](sceptre-example/).
+
+```yaml
+template:
+    # To use the CDK handler, you should use the "cdk" template type
+    type: cdk
+    # The path is always within your project's templates/ directory.
+    path: lambda_stack.py
+    deployment_type: bootstrapless
+    # bootstrapless_config are the snake_cased arguments passed to the cdk-bootstrapless-synthesizer
+    # for definitions of possible parameters, see the API docs here:
+    # https://github.com/aws-samples/cdk-bootstrapless-synthesizer/blob/main/API.md
+    bootstrapless_config:
+        # You can use !stack_attr to reference other stack attributes that happen
+        # to be set with resolvers to chain the resolver value. It makes sense to use the
+        # same bucket as Sceptre uses for its template uploads for your file assets.
+        file_asset_bucket_name: !stack_attr template_bucket_name
+        # It can be useful to apply the same prefix as your template_key_prefix to ensure your
+        # assets are namespaced similarly to the rest of Sceptre's uploaded artifacts.
+        file_asset_prefix: {{template_key_prefix}}/cdk-assets
+        # You can use !stack_output (and other resolvers) in all of these configurations, which is
+        # especially helpful when "wiring together" this stack with other stacks deployed in your
+        # environment.
+        image_asset_repository_name: !stack_output ecr.yaml::RepoName
+    # You can explicitly define your stack name, or use the default class name "CdkStack"
+    class_name: MyLambdaStack
+
+# Parameters are DEPLOY-TIME values passed to the CloudFormation template. Your CDK stack construct
+# needs to have CfnParameters in order to support this, though.
+parameters:
+    SpecialEnv: "This is my Sceptre test"
+
+# sceptre_user_data is passed to your Stack Class's constructor for supplying values at COMPILE-TIME.
+sceptre_user_data:
+    special_variable: "use this directly at compile_time"
+```
 
 ### Arguments:
 
-* `path` (string, required): The path to the CDK template.
-* `class_name` (string, optional): The name of the Class in the CDK template to synthesize. Default: `CdkStack`.
-* `context` (object, optional): The context for the CDK Stack. See [CDK Context](https://docs.aws.amazon.com/cdk/v2/guide/context.html) for further info.
+* `path` (string, required): The path to the CDK template file, relative to the `templates/` directory of
+  your project. This should be a python file with your stack class, subclassing `SceptreCdkStack`.
+* `deployment_type` (string, required): This determines the way CDK assets should be pushed to the
+  cloud. Options are `"bootstrapless"` and `"bootstrapped"`. See section above on "How to use" for
+  more details.
+* `bootstrap_qualifier` (string, optional): This is only used if you are using the `bootstrapped`
+  deployment type. This qualifier refers to the qualifier on a given CDK bootstrap stack in your
+  AWS account, whether deployed via CDK externally or within the same Sceptre project. If you use
+  the `bootstrapped` deployment_type and do NOT specify a qualifier, CDK will default to the default
+  qualifier and look to use that in your account.
+* `class_name` (string, optional): The name of the class on your CDK template to synthesize.
+  Defaults to `CdkStack`.
+* `context` (dict, optional): The context for the CDK Stack. See [CDK Context](https://docs.aws.amazon.com/cdk/v2/guide/context.html)
+for further info on this.
+* `bootstrapless_config` (dict, optional): This is only used if you are using the `bootstrapless`
+deployment type. The keys here are the snake-casings of the documented parameters using the
+[cdk-bootstrapless-synthesizer](https://github.com/aws-samples/cdk-bootstrapless-synthesizer/blob/main/API.md):
+    - "file_asset_bucket_name" (required if your stack has file assets)
+    - "file_asset_prefix"
+    - "file_asset_publishing_role_arn"
+    - "file_asset_region_set"
+    - "image_asset_account_id"
+    - "image_asset_publishing_role_arn"
+    - "image_asset_region_set"
+    - "image_asset_repository_name" (required if your stack has image assets)
+    - "image_asset_tag_prefix"
+    - "template_bucket_name"
 
 #### Passing Data to a CDK Stack
 
@@ -37,26 +224,34 @@ There are two methods for passing data to a CDK Stack; using `sceptre_user_data`
 
 ##### Sceptre User Data
 
-Data can be passed to a CDK stack using the `sceptre_user_data` block of the Sceptre stack config. This will be resolved
-when the template is synthesized and can contain complex objects.
+Data can be passed to a CDK stack using the `sceptre_user_data` block of the Sceptre stack config.
+This will be resolved when the template is synthesized and can contain complex objects. Since
+`sceptre_user_data` is a resolvable property, you can use Resolvers to pass values from other
+deployed stacks and other sources as well.
 
 ##### CloudFormation Parameters
 
-Data can be passed to a synthesized CDK template using standard CloudFormation parameters. These are resolved when the
-CloudFormation stack is created from the template, but can only contain string or list values as supported by Cloudformation.
+Data can be passed to a synthesized CDK template using standard CloudFormation parameters.
+These are resolved when the CloudFormation stack is created from the template, but can only contain
+string or list values as supported by Cloudformation. In order to use these, you need to create
+[`CfnParameter`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnParameter.html) resources
+in your stack class.
 
 #### Stack Outputs
 
-CloudFormation stack outputs can be defined in the CDK stack and then referenced from other Sceptre stacks using the
-standard Sceptre `!stack_output` resolver. 
+CloudFormation stack outputs can be defined in the CDK stack and then referenced from other Sceptre
+stacks using the standard Sceptre `!stack_output` resolver. In order to do this, your Stack Class
+will need to create [`CfnOutput`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnOutput.html)
+resources in your stack class.
 
 #### CDK Feature Flags and Custom Bootstrap
 
-To set any [CDK Feature Flags](https://docs.aws.amazon.com/cdk/v2/guide/featureflags.html) or to specify a modified
-[CDK Bootstrap](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) qualifier, set these in the handler's
-`context` argument. See [sceptre-example](sceptre-example) for an example of this.
+To set any [CDK Feature Flags](https://docs.aws.amazon.com/cdk/v2/guide/featureflags.html), set these
+in the handler's `context` argument. See [lambda-stack-bootstrapped.yaml](sceptre-example/config/lambda-stack-bootstrapped.yaml)
+for an example of this.
 
-Reminder: the `context` argument is a standard Sceptre resolvable property, so resolvers and/or Jinja variables can be used in the value. 
+Reminder: the `context` argument is a standard Sceptre resolvable property, so resolvers and/or Jinja
+variables can be used for values.
 
 ### How does this handler work?
 
@@ -69,8 +264,9 @@ bundle of required assets.
 3. Creates/updates a CloudFormation stack from the synthesized template.
 
 When you use Sceptre with this handler, the CDK handler performs steps 1-2 above to create a template
-that Sceptre can use, **but it does not use CDK to deploy it!**. Instead, Sceptre can use that template
-produced in step 1 above to perform all it's usual commands with all it's usual magic!
+that Sceptre can use and publish the assets, **but it does not use CDK to deploy it!**. Instead,
+Sceptre can use that template produced in step 1 above to perform all its usual commands with all
+it's usual magic!
 
 In other words, using this handler lets you use resolvers, put your CDK stack into StackGroups, let
 you name your stack according to Sceptre's naming conventions, `validate`, `diff`, and more! Basically,
@@ -78,21 +274,53 @@ the CDK stack can be managed using Sceptre just like any other.
 
 ### IAM and authentication
 
-This handler uses the stack's connection information to generate AWS environment variables and sets
-those on the CDK process, ensuring that the AWS authentication configuration on the stack config and
-project is carried over to CDK without any need for additional arguments.
-
-**Important:** CDK creates CloudFormation-ready templates and uses `cdk_assets` to publish artifacts
-to S3 and ECR in the process. This means that Sceptre commands that do not normally require S3 and ECR
-actions (such as `generate`, `validate`, `diff`, and others) will require them when using this
-handler. You will need to ensure that any user or role executing these commands has proper
-permissions for these operations.
-
-### Sceptre Management of the CDK Bootstrap
-
-To optionally manage the CDK bootstrap CloudFormation template and stack with Sceptre, the bootstrap
-template can be generated using the AWS CDK CLI: `cdk bootstrap --show-template > cdk-bootstrap.yaml`.
-This can be deployed into a stack using the standard Sceptre process. 
+There are several dimensions to how using this handler applies to IAM roles, policies, and permissions.
+
+#### The Role to deploy the CloudFormation Stacks themselves
+Sceptre will always use the AWS environment configuration, `profile` and/or `iam_role` to deploy
+the CloudFormation _Stacks_ themselves. This is consistent with how Sceptre always operates and it's
+no different when using this handler.
+
+If you want Sceptre to assume the Deployment Role from the CDK bootstrap stack, you'll need to
+specify that role as the `iam_role` for your stack(s). If you want to do this, it's recommended you
+add the deployment role as an output on your bootstrap stack and then set `iam_role` using a
+`!stack_output` or `!stack_output_external` resolver. Be aware that your current AWS environment's
+credentials will need permission to assume this role.
+
+For more information on the `iam_role` configuration, see [Sceptre docs on it](https://docs.sceptre-project.org/3.2.0/docs/stack_config.html#iam-role).
+
+#### The role provided to CloudFormation as the execution role
+By default, Sceptre doesn't provide an [execution/service role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html)
+to CloudFormation for stacks. This is a permanent action with implications you should be aware of.
+CDK _does_ tend to prefer using these, however, and one is created within the standard CDK bootstrap
+stack.
+
+If you want Sceptre to provide a service role to the stack, you'll need to specify that role as the
+stack's `role_arn`. Thus, you can use the Bootstrapped CloudFormation execution role if you want to
+or your organization generally requires that. If you want to do this, it's recommended you add the
+execution role as an output on your bootstrap stack and then set `role_arn` using `!stack_output` or
+`!stack_output_external`.
+
+For more information on the `role_arn` configuration, see [Sceptre docs on it](https://docs.sceptre-project.org/3.2.0/docs/stack_config.html#role-arn).
+
+#### The roles used to push file and image assets to the Cloud
+Some sorts of CDK constructs require the packaging and upload to the cloud of S3-destined file assets
+or ECR-destined image assets. These will each be published with different IAM permissions, depending
+on how you've configured your CDK handler.
+
+* If using the `"bootstrapless"` `deployment_type` and you have _not_ explicitly specified a
+`file_asset_publishing_role_arn` or `image_asset_publishing_role_arn`, CDK will use your configured
+Sceptre deployment role (if you have an iam_role specified), your profile (if specified), or your
+AWS environment credentials to push those credentials. If this is the case, be sure your credentials
+have permissions to perform those Put/Push operations.
+* If using the `"bootstrapless"` `deployment_type` and you _have_ explicitly specified a
+`file_asset_publishing_role_arn` and/or `image_asset_publishing_role_arn`, CDK will assume those
+roles (from your current deployment iam_role, profile, or AWS environment credentials) for their
+respective actions in order to perform those Put/Push operations. If this is the case, be sure that
+your iam_role, profile, or AWS environment credentials have permission to assume those roles.
+* If using the `"bootstrapped"` `deployment_type`, CDK will assume the respective roles from your
+bootstrap stack in order to perform those operations. If this is the case, be sure that
+your iam_role, profile, or AWS environment credentials have permission to assume those roles.
 
 ### Example Sceptre CDK Stack
 
diff --git a/pyproject.toml b/pyproject.toml
index c5982a3..e325be1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.coverage.run]
 branch = true
-source = [ "template_handler" ]
+source = [ "sceptre_cdk_handler" ]
 
 [tool.coverage.report]
 exclude_lines = [ ]
diff --git a/requirements.txt b/requirements.txt
index b16fc78..ba6fd18 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,10 +2,12 @@ bumpversion==0.5.3
 pytest-cov>=4.0.0
 pre-commit>=2.12.0,<2.13
 pytest>=7.0.0
+pytest-subtests
 readme-renderer>=24.0
 setuptools>=40.6.2
 sceptre>=2.7
 tox>=2.9.1,<3.0.0
 twine>=1.12.1
 wheel==0.32.3
-aws_cdk-lib>=2.51.1
+aws_cdk-lib>=2.0,<3.0
+cdk-bootstrapless-synthesizer>=2.0,<3.0
diff --git a/sceptre-example/config/assets-bucket.yaml b/sceptre-example/config/assets-bucket.yaml
new file mode 100644
index 0000000..4fe764d
--- /dev/null
+++ b/sceptre-example/config/assets-bucket.yaml
@@ -0,0 +1,10 @@
+# This is the template bucket for Sceptre templates. This is optional for Sceptre. However, if using
+# the "bootstrapless" deployment type, you will be required to specify a file_assets_bucket_name, so
+# it makes sense to use the same bucket as your template bucket.
+template:
+  path: assets-bucket.yaml
+
+# per Sceptre docs, if you create the template bucket and use that in the project-level config.yaml
+# the template-bucket stack can't have a template_bucket_name (otherwise you'd have a circular
+# dependency)
+template_bucket_name: !no_value
diff --git a/sceptre-example/config/cdk-bootstrap.yaml b/sceptre-example/config/cdk-bootstrap.yaml
new file mode 100644
index 0000000..1a96e87
--- /dev/null
+++ b/sceptre-example/config/cdk-bootstrap.yaml
@@ -0,0 +1,11 @@
+# This is the StackConfig for the bootstrap stack. The "bootstrapped" deployment_type of the cdk
+# handler requires a bootstrap stack with a matching qualifier. It can be deployed via Sceptre or
+# CDK. This is an example of deploying it via Sceptre.
+#
+# Note: You don't need a bootstrap stack if you use the "bootstrapless" deployment_type on the cdk
+# handler.
+template:
+  path: "cdk-bootstrap.yaml"
+
+parameters:
+  Qualifier: testing123
diff --git a/sceptre-example/config/dev/config.yaml b/sceptre-example/config/config.yaml
similarity index 52%
rename from sceptre-example/config/dev/config.yaml
rename to sceptre-example/config/config.yaml
index cce559c..3a04ad1 100644
--- a/sceptre-example/config/dev/config.yaml
+++ b/sceptre-example/config/config.yaml
@@ -1,12 +1,18 @@
-region: eu-west-1
-project_code: sceptre
-account_id: 123456789012
-profile: account1
-cdk_context:
-  # CDK Bootstrap Qualifier
-  '@aws-cdk/core:bootstrapQualifier': hnb659fds
+profile: cnc
+project_code: cdk-handler-test
+region: us-west-2
+template_bucket_name: !stack_output assets-bucket.yaml::BucketName
+template_key_prefix: "templates"
+# If you use an iam_role, it will be the default role used to deploy when using the bootstrapless
+# deployment_type (unless a different role is specified in the bootstrapless_config). If using the
+# "bootstrapped" deployment_type with an iam_role, that role will first be assumed and then CDK will
+# assume the bootstrapped roles for pushing image assets. The iam_role will always be used for
+# actually deploying the synthesized stack templates, however.
+iam_role: "arn:aws:iam::12345643232:role/my-deployment-role"
 
-  # CDK Feature Flags
+# If you plan to specify the CDK context for a number of stacks, you can add it as a custom config
+# on a StackGroup Config and then reference it on the CDK-based stacks.
+cdk_context:
   '@aws-cdk/aws-lambda:recognizeLayerVersion': true
   '@aws-cdk/core:checkSecretUsage': true
   '@aws-cdk/core:target-partitions':
diff --git a/sceptre-example/config/dev/cdk.yaml b/sceptre-example/config/dev/cdk.yaml
deleted file mode 100644
index e498c59..0000000
--- a/sceptre-example/config/dev/cdk.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-template:
-  type: cdk
-  path: CDK/s3.py
-  context: {{ cdk_context }}
-  class_name: CdkStack
-sceptre_user_data:
-  object_name: object-key.txt
-parameters:
-  BucketName: mybucket-{{ account_id }}-{{ region }}
diff --git a/sceptre-example/config/ecr.yaml b/sceptre-example/config/ecr.yaml
new file mode 100644
index 0000000..99478cd
--- /dev/null
+++ b/sceptre-example/config/ecr.yaml
@@ -0,0 +1,5 @@
+# This is the StackConfig for the ECR repo to push image assets to. You don't need this if you're
+# using the "bootstrapped" deployment_type on the cdk handler. You also don't need it when using the
+# "bootstrapless" deployment_type if you aren't building any CDK resources that produce ECR image assets.
+template:
+  path: ecr.yaml
diff --git a/sceptre-example/config/lambda-stack-bootstrapless.yaml b/sceptre-example/config/lambda-stack-bootstrapless.yaml
new file mode 100644
index 0000000..ce95379
--- /dev/null
+++ b/sceptre-example/config/lambda-stack-bootstrapless.yaml
@@ -0,0 +1,39 @@
+# This is an example of deploying the CDK stack using the "bootstrapless" deployment_type. This is
+# the more "Sceptre-like" way to configure your stacks. You can "wire together" other stacks into
+# this one using resolvers to populate values in the bootstrapless_config, parameters, and
+# sceptre_user_data. By default, the profile or role doing the deployments will also be used to push
+# the file and image assets.
+template:
+  type: cdk
+  # The path is always within your project's templates/ directory.
+  path: lambda_stack.py
+  deployment_type: bootstrapless
+  # bootstrapless_config are the snake_cased arguments passed to the cdk-bootstrapless-synthesizer
+  # for definitions of possible parameters, see the API docs here:
+  # https://github.com/aws-samples/cdk-bootstrapless-synthesizer/blob/main/API.md
+  bootstrapless_config:
+    # You can use !stack_attr to reference other stack attributes that happen
+    # to be set with resolvers to chain the resolver value. It makes sense to use the
+    # same bucket as Sceptre uses for its template uploads for your file assets.
+    file_asset_bucket_name: !stack_attr template_bucket_name
+    # It can be useful to apply the same prefix as your template_key_prefix to ensure your
+    # assets are namespaced similarly to the rest of Sceptre's uploaded artifacts.
+    file_asset_prefix: {{template_key_prefix}}/cdk-assets
+    image_asset_repository_name: !stack_output ecr.yaml::RepoName
+  # You can explicitly define your stack name
+  class_name: MyLambdaStack
+  # You can also pass CDK context key/value pairs. If you don't specify a context, the default
+  # context will be used (as determined by CDK). If you specify the context on the StackGroup config
+  # you can reference it via Jinja2 syntax like below.
+  context: {{cdk_context}}
+
+
+
+# Parameters are DEPLOY-TIME values passed to the CloudFormation template. Your CDK stack construct
+# needs to have CfnParameters in order to support this, though.
+parameters:
+  SpecialEnv: "This is my Sceptre test"
+
+# sceptre_user_data is passed to your Stack Class's constructor for supplying values at COMPILE-TIME.
+sceptre_user_data:
+  special_variable: "use this directly at compile_time"
diff --git a/sceptre-example/config/lambda-stack-bootstrapped.yaml b/sceptre-example/config/lambda-stack-bootstrapped.yaml
new file mode 100644
index 0000000..81b15b4
--- /dev/null
+++ b/sceptre-example/config/lambda-stack-bootstrapped.yaml
@@ -0,0 +1,58 @@
+# This is an example of deploying the CDK using the "bootstrapped" deployment_type, which means
+# using the resources from the CDK boostrap stack for things such as the asset bucket, asset ECR
+# repo, as well as the roles used to push those assets to the cloud. You can use either a Bootstrap
+# stack deployed via Sceptre or an existing one deployed to your account via CDK. All you need to
+# do to reference a given bootstrap stack in that environment is to use the bootstrap stack's
+# qualifier as the "bootstrap_qualifier". If you don't use a bootstrap_qualifier, CDK will default
+# to the default qualifier and try to use a bootstrap stack deployed in your account that has that
+# qualifier.
+#
+# Regardless of how the bootstrap stack is deployed, it should be generated by `cdk bootstrap
+# --show-template` to ensure maximum compatibility.
+#
+# IMPORTANT: Even when using the "bootstrapped" deployment_type, Sceptre WILL NOT use the actual
+# bootstrapped deployment role to deploy the CloudFormation stack; It will use its own configured
+# deployment environment, profile, and/or iam_role.
+template:
+  type: cdk
+  # The path is always within your project's templates/ directory.
+  path: lambda_stack.py
+  deployment_type: bootstrapped
+  # bootstrap_qualifier is optional, but it's better to be explicit about it, even if you do
+  # want to use the "default" bootstrap stack in your account. Using !stack_output here ensures that
+  # we have a dependency between this stack and the bootstrap stack within Sceptre. If you don't
+  # add that output to the bootstrap stack, you can just pass the qualifier as a string here, but
+  # then you'll want to add an explicit dependency via `dependencies`. See note below on that.
+  bootstrap_qualifier: !stack_output cdk-bootstrap.yaml::Qualifier
+  # You can also pass CDK context key/value pairs. If you don't specify a context, the default
+  # context will be used (as determined by CDK). If you specify the context on the StackGroup config
+  # you can reference it via Jinja2 syntax like below.
+  context: {{cdk_context}}
+  # You can explicitly define your stack name
+  class_name: MyLambdaStack
+
+# Parameters are DEPLOY-TIME values passed to the CloudFormation template. Your CDK stack construct
+# needs to have CfnParameters in order to support this, though.
+parameters:
+    SpecialEnv: "This is my Sceptre test"
+
+# sceptre_user_data is passed to your Stack Class's constructor for supplying values at COMPILE-TIME.
+sceptre_user_data:
+    special_variable: "use this directly at compile_time"
+
+# If you deploy the bootstrap stack via Sceptre but DON'T add outputs for the qualifier to reference
+# via !stack_output, you'll need to explicitly add a dependency this way so Sceptre knows to deploy
+# the bootstrap stack FIRST, before deploying this stack.
+#dependencies:
+#  cdk-bootstrap.yaml
+
+# If you want to use the bootstrapped deployment action role, you'll need to set that as the iam_role
+# But be careful if you do, since by default, the DeploymentActionRole doesn't actually have the
+# right permissions to deploy most things. Setting the iam_role means you'll assume the deployment
+# role to execute the cloudformation actions
+# iam_role: !stack_output cdk-bootstrap.yaml::DeployRoleArn
+
+# To work the way CDK bootstrap generally operates will mean you need to assign the cloudformation
+# execution role to the stack from the bootstrap stack. This will be assumed by CloudFormation to
+# actually deploy the infrastructure, permanently binding this role to the stack.
+#role_arn: !stack_output cdk-bootstrap.yaml::CloudformationExecutionRoleArn
diff --git a/sceptre-example/package.json b/sceptre-example/package.json
new file mode 100644
index 0000000..8ed78fe
--- /dev/null
+++ b/sceptre-example/package.json
@@ -0,0 +1,6 @@
+{
+  "dependencies": {
+    "aws-cdk": "^2.54.0",
+    "cdk-assets": "^2.54.0"
+  }
+}
diff --git a/sceptre-example/requirements.txt b/sceptre-example/requirements.txt
new file mode 100644
index 0000000..da155b6
--- /dev/null
+++ b/sceptre-example/requirements.txt
@@ -0,0 +1,3 @@
+sceptre
+aws-cdk.aws-lambda-python-alpha
+sceptre-cdk-handler
diff --git a/sceptre-example/templates/assets-bucket.yaml b/sceptre-example/templates/assets-bucket.yaml
new file mode 100644
index 0000000..3f4d6a8
--- /dev/null
+++ b/sceptre-example/templates/assets-bucket.yaml
@@ -0,0 +1,14 @@
+# This is a assets bucket that we can use for All of Sceptre's templates via the
+# template_bucket_name. We can use this for the file_asset_bucket_name for the bootstrapless
+# deployment_type
+AWSTemplateFormatVersion: "2010-09-09"
+
+Resources:
+    Bucket:
+        Type: AWS::S3::Bucket
+        Properties: {}
+
+
+Outputs:
+    BucketName:
+        Value: !Ref Bucket
diff --git a/sceptre-example/templates/cdk-bootstrap.yaml b/sceptre-example/templates/cdk-bootstrap.yaml
new file mode 100644
index 0000000..9d95ec4
--- /dev/null
+++ b/sceptre-example/templates/cdk-bootstrap.yaml
@@ -0,0 +1,602 @@
+# This bootstrap template was 99.9% just what was output by `cdk bootstrap --show-template > cdk-bootstrap.yaml`
+# except the added outputs at the end to simplfiy stack configurations. If you already
+# have a bootstrap stack in place in your environment, you can use that too. Having a bootstrap
+# stack in place (deployed via Sceptre or CDK) is required in order to support the "bootstrapped"
+# deployment_type for this handler.
+#
+# NOTE: You don't actually have to use a bootstrap stack at all if you use the bootstrapless
+# deployment_type.
+Description: This stack includes resources needed to deploy AWS CDK apps into this environment
+Parameters:
+  TrustedAccounts:
+    Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment
+    Default: ""
+    Type: CommaDelimitedList
+  TrustedAccountsForLookup:
+    Description: List of AWS accounts that are trusted to look up values in this environment
+    Default: ""
+    Type: CommaDelimitedList
+  CloudFormationExecutionPolicies:
+    Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role
+    Default: ""
+    Type: CommaDelimitedList
+  FileAssetsBucketName:
+    Description: The name of the S3 bucket used for file assets
+    Default: ""
+    Type: String
+  FileAssetsBucketKmsKeyId:
+    Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed S3 key, or the ID/ARN of an existing key.
+    Default: ""
+    Type: String
+  ContainerAssetsRepositoryName:
+    Description: A user-provided custom name to use for the container assets ECR repository
+    Default: ""
+    Type: String
+  Qualifier:
+    Description: An identifier to distinguish multiple bootstrap stacks in the same environment
+    Default: hnb659fds
+    Type: String
+    AllowedPattern: "[A-Za-z0-9_-]{1,10}"
+    ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters
+  PublicAccessBlockConfiguration:
+    Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration
+    Default: "true"
+    Type: String
+    AllowedValues:
+      - "true"
+      - "false"
+  InputPermissionsBoundary:
+    Description: Whether or not to use either the CDK supplied or custom permissions boundary
+    Default: ""
+    Type: String
+  UseExamplePermissionsBoundary:
+    Default: "false"
+    AllowedValues:
+      - "true"
+      - "false"
+    Type: String
+Conditions:
+  HasTrustedAccounts:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Fn::Join:
+              - ""
+              - Ref: TrustedAccounts
+  HasTrustedAccountsForLookup:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Fn::Join:
+              - ""
+              - Ref: TrustedAccountsForLookup
+  HasCloudFormationExecutionPolicies:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Fn::Join:
+              - ""
+              - Ref: CloudFormationExecutionPolicies
+  HasCustomFileAssetsBucketName:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Ref: FileAssetsBucketName
+  CreateNewKey:
+    Fn::Equals:
+      - ""
+      - Ref: FileAssetsBucketKmsKeyId
+  UseAwsManagedKey:
+    Fn::Equals:
+      - AWS_MANAGED_KEY
+      - Ref: FileAssetsBucketKmsKeyId
+  ShouldCreatePermissionsBoundary:
+    Fn::Equals:
+      - "true"
+      - Ref: UseExamplePermissionsBoundary
+  PermissionsBoundarySet:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Ref: InputPermissionsBoundary
+  HasCustomContainerAssetsRepositoryName:
+    Fn::Not:
+      - Fn::Equals:
+          - ""
+          - Ref: ContainerAssetsRepositoryName
+  UsePublicAccessBlockConfiguration:
+    Fn::Equals:
+      - "true"
+      - Ref: PublicAccessBlockConfiguration
+Resources:
+  FileAssetsBucketEncryptionKey:
+    Type: AWS::KMS::Key
+    Properties:
+      KeyPolicy:
+        Statement:
+          - Action:
+              - kms:Create*
+              - kms:Describe*
+              - kms:Enable*
+              - kms:List*
+              - kms:Put*
+              - kms:Update*
+              - kms:Revoke*
+              - kms:Disable*
+              - kms:Get*
+              - kms:Delete*
+              - kms:ScheduleKeyDeletion
+              - kms:CancelKeyDeletion
+              - kms:GenerateDataKey
+            Effect: Allow
+            Principal:
+              AWS:
+                Ref: AWS::AccountId
+            Resource: "*"
+          - Action:
+              - kms:Decrypt
+              - kms:DescribeKey
+              - kms:Encrypt
+              - kms:ReEncrypt*
+              - kms:GenerateDataKey*
+            Effect: Allow
+            Principal:
+              AWS: "*"
+            Resource: "*"
+            Condition:
+              StringEquals:
+                kms:CallerAccount:
+                  Ref: AWS::AccountId
+                kms:ViaService:
+                  - Fn::Sub: s3.${AWS::Region}.amazonaws.com
+          - Action:
+              - kms:Decrypt
+              - kms:DescribeKey
+              - kms:Encrypt
+              - kms:ReEncrypt*
+              - kms:GenerateDataKey*
+            Effect: Allow
+            Principal:
+              AWS:
+                Fn::Sub: ${FilePublishingRole.Arn}
+            Resource: "*"
+    Condition: CreateNewKey
+  FileAssetsBucketEncryptionKeyAlias:
+    Condition: CreateNewKey
+    Type: AWS::KMS::Alias
+    Properties:
+      AliasName:
+        Fn::Sub: alias/cdk-${Qualifier}-assets-key
+      TargetKeyId:
+        Ref: FileAssetsBucketEncryptionKey
+  StagingBucket:
+    Type: AWS::S3::Bucket
+    Properties:
+      BucketName:
+        Fn::If:
+          - HasCustomFileAssetsBucketName
+          - Fn::Sub: ${FileAssetsBucketName}
+          - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}
+      AccessControl: Private
+      BucketEncryption:
+        ServerSideEncryptionConfiguration:
+          - ServerSideEncryptionByDefault:
+              SSEAlgorithm: aws:kms
+              KMSMasterKeyID:
+                Fn::If:
+                  - CreateNewKey
+                  - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
+                  - Fn::If:
+                      - UseAwsManagedKey
+                      - Ref: AWS::NoValue
+                      - Fn::Sub: ${FileAssetsBucketKmsKeyId}
+      PublicAccessBlockConfiguration:
+        Fn::If:
+          - UsePublicAccessBlockConfiguration
+          - BlockPublicAcls: true
+            BlockPublicPolicy: true
+            IgnorePublicAcls: true
+            RestrictPublicBuckets: true
+          - Ref: AWS::NoValue
+      VersioningConfiguration:
+        Status: Enabled
+    UpdateReplacePolicy: Retain
+    DeletionPolicy: Retain
+  StagingBucketPolicy:
+    Type: AWS::S3::BucketPolicy
+    Properties:
+      Bucket:
+        Ref: StagingBucket
+      PolicyDocument:
+        Id: AccessControl
+        Version: "2012-10-17"
+        Statement:
+          - Sid: AllowSSLRequestsOnly
+            Action: s3:*
+            Effect: Deny
+            Resource:
+              - Fn::Sub: ${StagingBucket.Arn}
+              - Fn::Sub: ${StagingBucket.Arn}/*
+            Condition:
+              Bool:
+                aws:SecureTransport: "false"
+            Principal: "*"
+  ContainerAssetsRepository:
+    Type: AWS::ECR::Repository
+    Properties:
+      ImageTagMutability: IMMUTABLE
+      RepositoryName:
+        Fn::If:
+          - HasCustomContainerAssetsRepositoryName
+          - Fn::Sub: ${ContainerAssetsRepositoryName}
+          - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region}
+      RepositoryPolicyText:
+        Version: "2012-10-17"
+        Statement:
+          - Sid: LambdaECRImageRetrievalPolicy
+            Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action:
+              - ecr:BatchGetImage
+              - ecr:GetDownloadUrlForLayer
+            Condition:
+              StringLike:
+                aws:sourceArn:
+                  Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*
+  FilePublishingRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              AWS:
+                Ref: AWS::AccountId
+          - Fn::If:
+              - HasTrustedAccounts
+              - Action: sts:AssumeRole
+                Effect: Allow
+                Principal:
+                  AWS:
+                    Ref: TrustedAccounts
+              - Ref: AWS::NoValue
+      RoleName:
+        Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}
+      Tags:
+        - Key: aws-cdk:bootstrap-role
+          Value: file-publishing
+  ImagePublishingRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              AWS:
+                Ref: AWS::AccountId
+          - Fn::If:
+              - HasTrustedAccounts
+              - Action: sts:AssumeRole
+                Effect: Allow
+                Principal:
+                  AWS:
+                    Ref: TrustedAccounts
+              - Ref: AWS::NoValue
+      RoleName:
+        Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}
+      Tags:
+        - Key: aws-cdk:bootstrap-role
+          Value: image-publishing
+  LookupRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              AWS:
+                Ref: AWS::AccountId
+          - Fn::If:
+              - HasTrustedAccountsForLookup
+              - Action: sts:AssumeRole
+                Effect: Allow
+                Principal:
+                  AWS:
+                    Ref: TrustedAccountsForLookup
+              - Ref: AWS::NoValue
+          - Fn::If:
+              - HasTrustedAccounts
+              - Action: sts:AssumeRole
+                Effect: Allow
+                Principal:
+                  AWS:
+                    Ref: TrustedAccounts
+              - Ref: AWS::NoValue
+      RoleName:
+        Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}
+      ManagedPolicyArns:
+        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
+      Policies:
+        - PolicyDocument:
+            Statement:
+              - Sid: DontReadSecrets
+                Effect: Deny
+                Action:
+                  - kms:Decrypt
+                Resource: "*"
+            Version: "2012-10-17"
+          PolicyName: LookupRolePolicy
+      Tags:
+        - Key: aws-cdk:bootstrap-role
+          Value: lookup
+  FilePublishingRoleDefaultPolicy:
+    Type: AWS::IAM::Policy
+    Properties:
+      PolicyDocument:
+        Statement:
+          - Action:
+              - s3:GetObject*
+              - s3:GetBucket*
+              - s3:GetEncryptionConfiguration
+              - s3:List*
+              - s3:DeleteObject*
+              - s3:PutObject*
+              - s3:Abort*
+            Resource:
+              - Fn::Sub: ${StagingBucket.Arn}
+              - Fn::Sub: ${StagingBucket.Arn}/*
+            Effect: Allow
+          - Action:
+              - kms:Decrypt
+              - kms:DescribeKey
+              - kms:Encrypt
+              - kms:ReEncrypt*
+              - kms:GenerateDataKey*
+            Effect: Allow
+            Resource:
+              Fn::If:
+                - CreateNewKey
+                - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
+                - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId}
+        Version: "2012-10-17"
+      Roles:
+        - Ref: FilePublishingRole
+      PolicyName:
+        Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
+  ImagePublishingRoleDefaultPolicy:
+    Type: AWS::IAM::Policy
+    Properties:
+      PolicyDocument:
+        Statement:
+          - Action:
+              - ecr:PutImage
+              - ecr:InitiateLayerUpload
+              - ecr:UploadLayerPart
+              - ecr:CompleteLayerUpload
+              - ecr:BatchCheckLayerAvailability
+              - ecr:DescribeRepositories
+              - ecr:DescribeImages
+              - ecr:BatchGetImage
+              - ecr:GetDownloadUrlForLayer
+            Resource:
+              Fn::Sub: ${ContainerAssetsRepository.Arn}
+            Effect: Allow
+          - Action:
+              - ecr:GetAuthorizationToken
+            Resource: "*"
+            Effect: Allow
+        Version: "2012-10-17"
+      Roles:
+        - Ref: ImagePublishingRole
+      PolicyName:
+        Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
+  DeploymentActionRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              AWS:
+                Ref: AWS::AccountId
+          - Fn::If:
+              - HasTrustedAccounts
+              - Action: sts:AssumeRole
+                Effect: Allow
+                Principal:
+                  AWS:
+                    Ref: TrustedAccounts
+              - Ref: AWS::NoValue
+      Policies:
+        - PolicyDocument:
+            Statement:
+              - Sid: CloudFormationPermissions
+                Effect: Allow
+                Action:
+                  - cloudformation:CreateChangeSet
+                  - cloudformation:DeleteChangeSet
+                  - cloudformation:DescribeChangeSet
+                  - cloudformation:DescribeStacks
+                  - cloudformation:ExecuteChangeSet
+                  - cloudformation:CreateStack
+                  - cloudformation:UpdateStack
+                Resource: "*"
+              - Sid: PipelineCrossAccountArtifactsBucket
+                Effect: Allow
+                Action:
+                  - s3:GetObject*
+                  - s3:GetBucket*
+                  - s3:List*
+                  - s3:Abort*
+                  - s3:DeleteObject*
+                  - s3:PutObject*
+                Resource: "*"
+                Condition:
+                  StringNotEquals:
+                    s3:ResourceAccount:
+                      Ref: AWS::AccountId
+              - Sid: PipelineCrossAccountArtifactsKey
+                Effect: Allow
+                Action:
+                  - kms:Decrypt
+                  - kms:DescribeKey
+                  - kms:Encrypt
+                  - kms:ReEncrypt*
+                  - kms:GenerateDataKey*
+                Resource: "*"
+                Condition:
+                  StringEquals:
+                    kms:ViaService:
+                      Fn::Sub: s3.${AWS::Region}.amazonaws.com
+              - Action: iam:PassRole
+                Resource:
+                  Fn::Sub: ${CloudFormationExecutionRole.Arn}
+                Effect: Allow
+              - Sid: CliPermissions
+                Action:
+                  - cloudformation:DescribeStackEvents
+                  - cloudformation:GetTemplate
+                  - cloudformation:DeleteStack
+                  - cloudformation:UpdateTerminationProtection
+                  - sts:GetCallerIdentity
+                  - cloudformation:GetTemplateSummary
+                Resource: "*"
+                Effect: Allow
+              - Sid: CliStagingBucket
+                Effect: Allow
+                Action:
+                  - s3:GetObject*
+                  - s3:GetBucket*
+                  - s3:List*
+                Resource:
+                  - Fn::Sub: ${StagingBucket.Arn}
+                  - Fn::Sub: ${StagingBucket.Arn}/*
+              - Sid: ReadVersion
+                Effect: Allow
+                Action:
+                  - ssm:GetParameter
+                Resource:
+                  - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}
+            Version: "2012-10-17"
+          PolicyName: default
+      RoleName:
+        Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}
+      Tags:
+        - Key: aws-cdk:bootstrap-role
+          Value: deploy
+  CloudFormationExecutionRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              Service: cloudformation.amazonaws.com
+        Version: "2012-10-17"
+      ManagedPolicyArns:
+        Fn::If:
+          - HasCloudFormationExecutionPolicies
+          - Ref: CloudFormationExecutionPolicies
+          - Fn::If:
+              - HasTrustedAccounts
+              - Ref: AWS::NoValue
+              - - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
+      RoleName:
+        Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}
+      PermissionsBoundary:
+        Fn::If:
+          - PermissionsBoundarySet
+          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
+          - Ref: AWS::NoValue
+  CdkBoostrapPermissionsBoundaryPolicy:
+    Condition: ShouldCreatePermissionsBoundary
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      PolicyDocument:
+        Statement:
+          - Sid: ExplicitAllowAll
+            Action:
+              - "*"
+            Effect: Allow
+            Resource: "*"
+          - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
+            Action:
+              - iam:CreateUser
+              - iam:CreateRole
+              - iam:PutRolePermissionsBoundary
+              - iam:PutUserPermissionsBoundary
+            Condition:
+              StringNotEquals:
+                iam:PermissionsBoundary:
+                  Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
+            Effect: Deny
+            Resource: "*"
+          - Sid: DenyPermBoundaryIAMPolicyAlteration
+            Action:
+              - iam:CreatePolicyVersion
+              - iam:DeletePolicy
+              - iam:DeletePolicyVersion
+              - iam:SetDefaultPolicyVersion
+            Effect: Deny
+            Resource:
+              Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
+          - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
+            Action:
+              - iam:DeleteUserPermissionsBoundary
+              - iam:DeleteRolePermissionsBoundary
+            Effect: Deny
+            Resource: "*"
+        Version: "2012-10-17"
+      Description: Bootstrap Permission Boundary
+      ManagedPolicyName:
+        Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
+      Path: /
+  CdkBootstrapVersion:
+    Type: AWS::SSM::Parameter
+    Properties:
+      Type: String
+      Name:
+        Fn::Sub: /cdk-bootstrap/${Qualifier}/version
+      Value: "14"
+Outputs:
+  BucketName:
+    Description: The name of the S3 bucket owned by the CDK toolkit stack
+    Value:
+      Fn::Sub: ${StagingBucket}
+  BucketDomainName:
+    Description: The domain name of the S3 bucket owned by the CDK toolkit stack
+    Value:
+      Fn::Sub: ${StagingBucket.RegionalDomainName}
+  FileAssetKeyArn:
+    Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated)
+    Value:
+      Fn::If:
+        - CreateNewKey
+        - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
+        - Fn::Sub: ${FileAssetsBucketKmsKeyId}
+    Export:
+      Name:
+        Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn
+  ImageRepositoryName:
+    Description: The name of the ECR repository which hosts docker image assets
+    Value:
+      Fn::Sub: ${ContainerAssetsRepository}
+  BootstrapVersion:
+    Description: The version of the bootstrap resources that are currently mastered in this stack
+    Value:
+      Fn::GetAtt:
+        - CdkBootstrapVersion
+        - Value
+  # These outputs were added to make integration with Sceptre more straight-forward
+  Qualifier:
+    Value: !Ref Qualifier
+  DeployRoleArn:
+    Value: !GetAtt DeploymentActionRole.Arn
+  CloudformationExecutionRoleArn:
+    Value: !GetAtt CloudFormationExecutionRole.Arn
diff --git a/sceptre-example/templates/custom-image/dockerfile b/sceptre-example/templates/custom-image/dockerfile
new file mode 100644
index 0000000..1e8ca42
--- /dev/null
+++ b/sceptre-example/templates/custom-image/dockerfile
@@ -0,0 +1,2 @@
+# You can build and push custom dockerfiles to ECR. This is just a demonstration.
+FROM python:3.9
diff --git a/sceptre-example/templates/ecr.yaml b/sceptre-example/templates/ecr.yaml
new file mode 100644
index 0000000..e9fe69c
--- /dev/null
+++ b/sceptre-example/templates/ecr.yaml
@@ -0,0 +1,11 @@
+# This is just the simplest possible ECR repository to support image assets for the "bootstrapless"
+# deployment_type. You won't need this if you're using the "bootstrapped" deployment_type.
+AWSTemplateFormatVersion: "2010-09-09"
+
+Resources:
+    Repository:
+        Type: AWS::ECR::Repository
+
+Outputs:
+    RepoName:
+        Value: !Ref Repository
diff --git a/sceptre-example/templates/lambda_stack.py b/sceptre-example/templates/lambda_stack.py
new file mode 100644
index 0000000..a525f95
--- /dev/null
+++ b/sceptre-example/templates/lambda_stack.py
@@ -0,0 +1,38 @@
+from pathlib import Path
+from aws_cdk.aws_lambda import Runtime
+from aws_cdk.aws_lambda_python_alpha import PythonFunction
+from aws_cdk import CfnOutput, CfnParameter, aws_s3, aws_ecr_assets
+from sceptre_cdk_handler import SceptreCdkStack
+
+
+# Important: Notice how it subclasses SceptreCdkStack and passes **kwargs into the base class's
+# __init__(). This is important to maintain compability with the different deployment_types.
+class MyLambdaStack(SceptreCdkStack):
+    def __init__(self, scope, id: str, sceptre_user_data: dict, **kwargs):
+        super().__init__(scope, id, sceptre_user_data, **kwargs)
+        # If you want to pass parameters like you do elsewhere in Sceptre, this works great!
+        self.special_parameter = CfnParameter(self, 'SpecialEnv')
+        self.my_lambda = PythonFunction(
+            self,
+            'LambdaFunction',
+            entry=str(Path(__file__).parent / 'logging_lambda'),
+            runtime=Runtime.PYTHON_3_9,
+            handler='lambda_handler',
+            index="handler.py",
+            environment={
+                'SPECIAL_ENV': self.special_parameter.value_as_string,
+                # You can also access self.sceptre_user_data for compile-time variables
+                'OTHER_ENVIRONMENT_VARIABLE': self.sceptre_user_data['special_variable']
+            },
+        )
+        # Image asset uploads to ECR are fully supported as well
+        self.image_asset = aws_ecr_assets.DockerImageAsset(
+            self,
+            'CustomImage',
+            directory=str(Path(__file__).parent / 'custom-image')
+        )
+        self.bucket = aws_s3.Bucket(self, 'Bucket')
+
+        # If you want to get values out of your stack for Sceptre to pass values to other stacks,
+        # be sure to include CfnOutputs.
+        self.function_output = CfnOutput(self, 'OutputValue', value=self.my_lambda.function_arn)
diff --git a/sceptre-example/templates/logging_lambda/handler.py b/sceptre-example/templates/logging_lambda/handler.py
new file mode 100644
index 0000000..f303831
--- /dev/null
+++ b/sceptre-example/templates/logging_lambda/handler.py
@@ -0,0 +1,11 @@
+# This is a demonstration lambda to show that it along with its dependencies can be
+# packaged up and deployed.
+import requests
+import os
+
+
+def lambda_handler(event, context):
+    print(f'My special env: {os.getenv("SPECIAL_ENV")}')
+    response = requests.get('https://api.ipify.org?format=json')
+    if response.ok:
+        print(f'My ip: {response.json()["ip"]}')
diff --git a/sceptre-example/templates/logging_lambda/requirements.txt b/sceptre-example/templates/logging_lambda/requirements.txt
new file mode 100644
index 0000000..663bd1f
--- /dev/null
+++ b/sceptre-example/templates/logging_lambda/requirements.txt
@@ -0,0 +1,1 @@
+requests
\ No newline at end of file
diff --git a/sceptre_cdk_handler/__init__.py b/sceptre_cdk_handler/__init__.py
new file mode 100644
index 0000000..0846331
--- /dev/null
+++ b/sceptre_cdk_handler/__init__.py
@@ -0,0 +1,2 @@
+# import this up to make it easy to import for subclassing
+from .cdk_builder import SceptreCdkStack
diff --git a/sceptre_cdk_handler/cdk.py b/sceptre_cdk_handler/cdk.py
new file mode 100644
index 0000000..bbf3289
--- /dev/null
+++ b/sceptre_cdk_handler/cdk.py
@@ -0,0 +1,225 @@
+import pathlib
+from pathlib import Path
+from typing import Any, Optional, Tuple, Type
+
+import yaml
+from sceptre.connection_manager import ConnectionManager
+from sceptre.exceptions import SceptreException
+from sceptre.helpers import normalise_path
+from sceptre.template_handlers import TemplateHandler
+
+from sceptre_cdk_handler.cdk_builder import BootstrappedCdkBuilder, BootstraplessCdkBuilder, SceptreCdkStack
+from sceptre_cdk_handler.class_importer import ClassImporter
+from sceptre_cdk_handler.command_checker import CommandChecker
+
+try:
+    # Literal was defined in py3.8.
+    from typing import Literal
+except ImportError:
+    # If running this in py3.7, we'll need to instead import it from typing_extensions, which
+    # back-ports a lot of the typing constructs of later versions of Python.
+    from typing_extensions import Literal
+
+DEFAULT_CLASS_NAME = 'CdkStack'
+QUALIFIER_CONTEXT_KEY = '@aws-cdk/core:bootstrapQualifier'
+
+
+class CDK(TemplateHandler):
+    """
+    A template handler for AWS CDK templates. Using this will allow Sceptre to work with the AWS CDK to
+    build and package a CDK template and deploy it with Sceptre.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        arguments: dict = None,
+        sceptre_user_data: Any = None,
+        connection_manager: ConnectionManager = None,
+        stack_group_config: dict = None,
+        *,
+        importer_class=ClassImporter,
+        bootstrapped_cdk_builder_class=BootstrappedCdkBuilder,
+        bootstrapless_cdk_builder_class=BootstraplessCdkBuilder,
+        command_checker_class=CommandChecker
+    ):
+        super().__init__(
+            name=name,
+            arguments=arguments,
+            sceptre_user_data=sceptre_user_data,
+            connection_manager=connection_manager,
+            stack_group_config=stack_group_config
+        )
+        self._importer = importer_class()
+        self._bootstrapped_cdk_builder_class = bootstrapped_cdk_builder_class
+        self._bootstrapless_cdk_builder_class = bootstrapless_cdk_builder_class
+        self._command_checker = command_checker_class(self.logger)
+
+    def schema(self):
+        """
+        Return a JSON schema of the properties that this template handler requires.
+
+        Reference: https://github.com/Julian/jsonschema
+        """
+        return {
+            "type": "object",
+            "additionalProperties": False,
+            "properties": {
+                "path": {"type": "string"},
+                "deployment_type": {
+                    "type": "string",
+                    "enum": ["bootstrapped", "bootstrapless"]
+                },
+                "bootstrap_qualifier": {"type": "string"},
+                "context": {"type": "object"},
+                "class_name": {"type": "string"},
+                "bootstrapless_config": {
+                    "type": "object",
+                    "additionalProperties": False,
+                    "properties": {
+                        "file_asset_bucket_name": {"type": "string"},
+                        "file_asset_prefix": {"type": "string"},
+                        "file_asset_publishing_role_arn": {"type": "string"},
+                        "file_asset_region_set": {"type": "string"},
+                        "image_asset_account_id": {"type": "string"},
+                        "image_asset_publishing_role_arn": {"type": "string"},
+                        "image_asset_region_set": {"type": "string"},
+                        "image_asset_repository_name": {"type": "string"},
+                        "image_asset_tag_prefix": {"type": "string"},
+                        "template_bucket_name": {"type": "string"}
+                    },
+                }
+            },
+            "required": [
+                "path",
+                'deployment_type'
+            ]
+        }
+
+    @property
+    def cdk_template_path(self) -> Path:
+        """CDK Template file path, relative to the the project's templates directory"""
+
+        return self._resolve_template_path(self.arguments['path'])
+
+    def _resolve_template_path(self, template_path: str) -> Path:
+        """
+        Return the project_path joined to template_path as
+        a string.
+
+        Note that os.path.join defers to an absolute path
+        if the input is absolute.
+        """
+        return pathlib.Path(
+            self.stack_group_config["project_path"],
+            "templates",
+            normalise_path(template_path)
+        )
+
+    @property
+    def cdk_context(self) -> dict:
+        """The CDK context dict to pass to the app. Used to pass feature flags, etc..."""
+
+        return self.arguments.get('context')
+
+    @property
+    def cdk_class_name(self) -> str:
+        """The name of the CDK Stack class on the template file to import."""
+
+        return self.arguments.get('class_name', DEFAULT_CLASS_NAME)
+
+    @property
+    def bootstrap_qualifier(self) -> Optional[str]:
+        """The bootstrap stack qualifier to use for the "bootstrapped" deployment_type."""
+        return self.arguments.get('bootstrap_qualifier')
+
+    @property
+    def deployment_type(self) -> Literal['bootstrapped', 'bootstrapless']:
+        """The way Sceptre should handle the deployment of file and image assets. Can be one of
+        "bootstrapped" or "bootstrapless".
+        """
+        return self.arguments['deployment_type']
+
+    @property
+    def bootstrapless_config(self) -> dict:
+        """The synthesizer args for CDK Bootstrapless Synthesizer. Used only with the "bootstrapless"
+        deployment_type.
+        """
+        return self.arguments.get('bootstrapless_config', {})
+
+    def handle(self) -> str:
+        """
+        Main Sceptre CDK Handler function
+
+        Returns:
+            str - The CDK synthesised CloudFormation template
+        """
+        stack_class: Type[SceptreCdkStack] = self._importer.import_class(self.cdk_template_path, self.cdk_class_name)
+        if self.deployment_type == 'bootstrapped':
+            context, builder = self._get_bootstrapped_builder()
+        elif self.deployment_type == "bootstrapless":
+            builder = self._get_bootstrapless_builder()
+            context = self.cdk_context
+        else:
+            # It shouldn't be possible to get here due to the json schema validation
+            raise ValueError("deployment_type must be 'bootstrapped' or 'bootstrapless'")
+
+        template_dict = builder.build_template(
+            stack_class,
+            context,
+            self.sceptre_user_data
+        )
+        return yaml.safe_dump(template_dict)
+
+    def _get_bootstrapped_builder(self) -> Tuple[Optional[dict], BootstrappedCdkBuilder]:
+        builder = self._bootstrapped_cdk_builder_class(self.logger, self.connection_manager)
+        # The qualifier might already be in the context, in which case we don't need to do
+        # anything
+        if self.cdk_context and QUALIFIER_CONTEXT_KEY in self.cdk_context:
+            return self.cdk_context, builder
+        # As a convenience, the qualifier can be set as its own argument to simplify the
+        # configuration. If it's passed this way, we need to add it to whatever context dict there
+        # is, if one exists; We'll make one if it doesn't.
+        if self.bootstrap_qualifier:
+            context = self.cdk_context or {}
+            context[QUALIFIER_CONTEXT_KEY] = self.bootstrap_qualifier
+            return context, builder
+        # If there's no qualifier specified anywhere, we're falling back to either the specified
+        # context or the default CDK context (if no context is specified on the handler)
+        return self.cdk_context, builder
+
+    def _get_bootstrapless_builder(self) -> BootstraplessCdkBuilder:
+        return self._bootstrapless_cdk_builder_class(
+            self.logger,
+            self.connection_manager,
+            self.bootstrapless_config
+        )
+
+    def validate(self):
+        super().validate()
+        self._check_prerequisites()
+
+    def _check_prerequisites(self) -> None:
+        """
+        Checks the command and Node package requirements for the handler.
+
+        Raises:
+            SceptreException: Command prerequisite not found
+            SceptreException: Node Package prerequisite not found
+        """
+        # Check Command Prerequisites
+        cmd_prerequisites = [
+            'node',
+            'npx'
+        ]
+        for cmd_prerequisite in cmd_prerequisites:
+            if not self._command_checker.cmd_exists(cmd_prerequisite):
+                raise SceptreException(f"Command prerequisite '{cmd_prerequisite}' not found")
+
+        # Check Node Package Prerequisites
+        node_prerequisites = [
+            'cdk-assets'
+        ]
+        for node_prerequisite in node_prerequisites:
+            if not self._command_checker.node_package_exists(node_prerequisite):
+                raise SceptreException(f"Node Package prerequisite '{node_prerequisite}' not found")
diff --git a/sceptre_cdk_handler/cdk_builder.py b/sceptre_cdk_handler/cdk_builder.py
new file mode 100644
index 0000000..11884b4
--- /dev/null
+++ b/sceptre_cdk_handler/cdk_builder.py
@@ -0,0 +1,218 @@
+import logging
+import os
+import subprocess
+import sys
+from abc import ABC, abstractmethod
+from typing import Any, Optional, Dict, Type
+
+import aws_cdk
+from aws_cdk.cx_api import CloudAssembly
+from botocore.credentials import Credentials
+from cdk_bootstrapless_synthesizer import BootstraplessStackSynthesizer
+from sceptre import exceptions
+from sceptre.connection_manager import ConnectionManager
+from sceptre.exceptions import TemplateHandlerArgumentsInvalidError
+
+
+class SceptreCdkStack(aws_cdk.Stack):
+    def __init__(self, scope: aws_cdk.App, id: str, sceptre_user_data: Any, **kwargs):
+        super().__init__(scope, id, **kwargs)
+        self.sceptre_user_data = sceptre_user_data
+
+
+class CdkBuilder(ABC):
+    """A base class for CDK builders to define the interface they all must meet."""
+    STACK_LOGICAL_ID = 'CDKStack'
+
+    @abstractmethod
+    def build_template(
+        self,
+        stack_class: Type[SceptreCdkStack],
+        cdk_context: Optional[dict],
+        sceptre_user_data: Any
+    ) -> dict: ...
+
+
+class BootstrappedCdkBuilder(CdkBuilder):
+    """A CdkBuilder for stacks utilizing the CDK bootstrap stack for asset-related actions."""
+    def __init__(
+        self,
+        logger: logging.Logger,
+        connection_manager: ConnectionManager,
+        *,
+        subprocess_run=subprocess.run,
+        app_class=aws_cdk.App,
+        environment_variables=os.environ
+    ):
+        self._logger = logger
+        self._connection_manager = connection_manager
+        self._subprocess_run = subprocess_run
+        self._app_class = app_class
+        self._environment_variables = environment_variables
+
+    def build_template(
+        self,
+        stack_class: Type[SceptreCdkStack],
+        cdk_context: Optional[dict],
+        sceptre_user_data: Any
+    ) -> dict:
+        assembly = self._synthesize(stack_class, cdk_context, sceptre_user_data)
+        self._publish(assembly)
+        template = self._get_template(assembly)
+        return template
+
+    def _synthesize(
+        self,
+        stack_class: Type[SceptreCdkStack],
+        cdk_context: Optional[dict],
+        sceptre_user_data: Any
+    ) -> CloudAssembly:
+        self._logger.debug(f'CDK synthesizing stack class: {stack_class.__name__}')
+        self._logger.debug(f'CDK Context: {cdk_context}')
+        app = self._app_class(context=cdk_context)
+        stack_class(app, self.STACK_LOGICAL_ID, sceptre_user_data)
+        return app.synth()
+
+    def _publish(self, cloud_assembly: CloudAssembly):
+        asset_artifacts = self._get_assets_manifest(cloud_assembly)
+        if self._only_asset_is_template(asset_artifacts):
+            # Sceptre already has a mechanism to upload the template if configured. We don't
+            # need to deploy assets if the only asset is the template
+            self._logger.debug("Only asset is template; Skipping asset upload.")
+            return
+
+        environment_variables = self._get_envs()
+        self._logger.info('Publishing CDK assets')
+        self._logger.debug(f'Assets manifest file: {asset_artifacts.file}')
+        self._run_command(
+            f'npx cdk-assets -v publish --path {asset_artifacts.file}',
+            env=environment_variables
+        )
+
+    def _get_assets_manifest(self, cloud_assembly):
+        asset_artifacts = None
+        for artifacts in cloud_assembly.artifacts:
+            if isinstance(artifacts, aws_cdk.cx_api.AssetManifestArtifact):
+                asset_artifacts = artifacts
+                break
+        if asset_artifacts is None:
+            raise exceptions.SceptreException('CDK Asset manifest artifact not found')
+        return asset_artifacts
+
+    def _get_template(self, cloud_assembly: CloudAssembly) -> dict:
+        return cloud_assembly.get_stack_by_name(self.STACK_LOGICAL_ID).template
+
+    def _run_command(self, command: str, env: Dict[str, str] = None):
+        # We're assuming here that the cwd is the directory to run the command from. I'm not certain
+        # that will always be correct...
+        result = self._subprocess_run(
+            command,
+            env=env,
+            shell=True,
+            stdout=sys.stderr,
+            check=True
+        )
+
+        return result
+
+    def _get_envs(self) -> Dict[str, str]:
+        """
+        Obtains the environment variables to pass to the subprocess.
+
+        Sceptre can assume roles, profiles, etc... to connect to AWS for a given stack. This is
+        very useful. However, we need that SAME connection information to carry over to CDK when we
+        invoke it. The most precise way to do this is to use the same session credentials being used
+        by Sceptre for other stack operations. This method obtains those credentials and sets them
+        as environment variables that are passed to the subprocess and will, in turn, be used by
+        SAM CLI.
+
+        The environment variables dict created by this method will inherit all existing
+        environment variables in the current environment, but the AWS connection environment
+        variables will be overridden by the ones for this stack.
+
+        Returns:
+            The dictionary of environment variables.
+        """
+        envs = self._environment_variables.copy()
+        envs.pop("AWS_PROFILE", None)
+        # Set aws environment variables specific to whatever AWS configuration has been set on the
+        # stack's connection manager.
+        credentials: Credentials = self._connection_manager._get_session(
+            self._connection_manager.profile,
+            self._connection_manager.region,
+            self._connection_manager.iam_role
+        ).get_credentials()
+        envs.update(
+            AWS_ACCESS_KEY_ID=credentials.access_key,
+            AWS_SECRET_ACCESS_KEY=credentials.secret_key,
+            # Most AWS SDKs use AWS_DEFAULT_REGION for the region
+            AWS_DEFAULT_REGION=self._connection_manager.region,
+            # CDK frequently uses CDK_DEFAULT_REGION in its docs
+            CDK_DEFAULT_REGION=self._connection_manager.region,
+            # cdk-assets requires AWS_REGION to determine what region's STS endpoint to use
+            AWS_REGION=self._connection_manager.region
+        )
+
+        # There might not be a session token, so if there isn't one, make sure it doesn't exist in
+        # the envs being passed to the subprocess
+        if credentials.token is None:
+            envs.pop('AWS_SESSION_TOKEN', None)
+        else:
+            envs['AWS_SESSION_TOKEN'] = credentials.token
+
+        return envs
+
+    def _only_asset_is_template(self, asset_artifacts: aws_cdk.cx_api.AssetManifestArtifact):
+        manifest_contents = asset_artifacts.contents
+        if manifest_contents.docker_images:
+            return False
+
+        keys = list(manifest_contents.files.keys())
+        expected_template = f'{self.STACK_LOGICAL_ID}.template.json'
+        return keys == [expected_template]
+
+
+class BootstraplessCdkBuilder(BootstrappedCdkBuilder):
+    """A CdkBuilder that does not use the CDK bootstrap stack for asset actions; Instead, specific
+    asset-required resources can be specified in the synthesizer_config.
+    """
+    def __init__(
+        self,
+        logger: logging.Logger,
+        connection_manager: ConnectionManager,
+        synthesizer_config: dict,
+        *,
+        subprocess_run=subprocess.run,
+        app_class=aws_cdk.App,
+        environment_variables=os.environ,
+        synthesizer_class=BootstraplessStackSynthesizer
+    ):
+        super().__init__(
+            logger,
+            connection_manager,
+            subprocess_run=subprocess_run,
+            app_class=app_class,
+            environment_variables=environment_variables
+        )
+        self._synthesizer_config = synthesizer_config
+        self._synthesizer_class = synthesizer_class
+
+    def _synthesize(
+        self,
+        stack_class: Type[SceptreCdkStack],
+        cdk_context: Optional[dict],
+        sceptre_user_data: Any
+    ) -> CloudAssembly:
+        self._logger.debug(f'CDK synthesizing stack class: {stack_class.__name__}')
+        self._logger.debug(f'CDK Context: {cdk_context}')
+        app = self._app_class(context=cdk_context)
+        try:
+            synthesizer = self._synthesizer_class(**self._synthesizer_config)
+        except TypeError as e:
+            raise TemplateHandlerArgumentsInvalidError(
+                "Error encountered attempting to instantiate the BootstraplessSynthesizer with the "
+                f"specified deployment config: {e}"
+            ) from e
+
+        stack_class(app, self.STACK_LOGICAL_ID, sceptre_user_data, synthesizer=synthesizer)
+        return app.synth()
diff --git a/sceptre_cdk_handler/class_importer.py b/sceptre_cdk_handler/class_importer.py
new file mode 100644
index 0000000..98963a6
--- /dev/null
+++ b/sceptre_cdk_handler/class_importer.py
@@ -0,0 +1,38 @@
+import importlib.machinery
+import importlib.util
+from pathlib import Path
+from typing import Type
+
+from sceptre import exceptions
+
+from sceptre_cdk_handler.cdk_builder import SceptreCdkStack
+
+
+class ClassImporter:
+    def import_class(self, template_path: Path, class_name: str) -> Type[SceptreCdkStack]:
+        """
+        Import the CDK Python template module.
+
+        Args:
+            template_path: The path of the CDK Template
+            class_name: The name of the class
+
+        Returns:
+            A ModuleType object containing the imported CDK Python module
+
+        Raises:
+            SceptreException: Template File not found
+            SceptreException: importlib general exception
+        """
+        template_module_name = template_path.stem
+        loader = importlib.machinery.SourceFileLoader(template_module_name, str(template_path))
+        spec = importlib.util.spec_from_loader(template_module_name, loader)
+        template_module = importlib.util.module_from_spec(spec)
+        loader.exec_module(template_module)
+
+        try:
+            return getattr(template_module, class_name)
+        except AttributeError:
+            raise exceptions.SceptreException(
+                f"No class named  {class_name} on template at {template_path}"
+            )
diff --git a/sceptre_cdk_handler/command_checker.py b/sceptre_cdk_handler/command_checker.py
new file mode 100644
index 0000000..90eff24
--- /dev/null
+++ b/sceptre_cdk_handler/command_checker.py
@@ -0,0 +1,50 @@
+import shutil
+import subprocess
+from logging import Logger
+
+
+class CommandChecker:
+    """A utility for checking if specific commands and node packages exist in the environment."""
+    def __init__(
+        self,
+        logger: Logger,
+        *,
+        subprocess_run=subprocess.run,
+        which_func=shutil.which
+    ):
+        self._logger = logger
+        self._subprocess_run = subprocess_run
+        self._which = which_func
+
+    def cmd_exists(self, cmd: str) -> bool:
+        """
+        Checks whether a specified CLI command exists.
+
+        Args:
+            cmd: The name of the CLI command to check
+
+        Returns:
+            bool
+        """
+
+        cmd_exists = self._which(cmd) is not None
+        self._logger.debug(f"command '{cmd}' exists: {cmd_exists}")
+        return cmd_exists
+
+    def node_package_exists(self, package: str) -> bool:
+        """
+        Checks whether a specific Node package exists either in the workspace or global scope.
+
+        Args:
+            package: str - The name of the node package to check
+        """
+        workspace_result = self._subprocess_run(f'npm list {package}', shell=True)
+        self._logger.debug(f"Workspace NPM package '{package}' exists: {not bool(workspace_result.returncode)}")
+
+        if workspace_result.returncode == 0:
+            return True
+
+        global_result = self._subprocess_run(f'npm --global list {package}', shell=True)
+        self._logger.debug(f"f'Global NPM package '{package}' exists: {not bool(global_result.returncode)}")
+
+        return global_result.returncode == 0
diff --git a/setup.py b/setup.py
index b18385e..ba8014d 100644
--- a/setup.py
+++ b/setup.py
@@ -7,11 +7,11 @@
 
 # lowercase, use `-` as separator.
 TEMPLATE_HANDLER_NAME = 'sceptre-cdk-handler'
-# the template_handler call in sceptre e.g. type: custom_template_handler.
+# the template_handler call in sceptre e.g. type: custom_sceptre_cdk_handler.
 TEMPLATE_HANDLER_TYPE = 'cdk'
-# do not change. Rename template_handler/template_handler.py to template_handler/{TEMPLATE_HANDLER_TYPE}.py
-TEMPLATE_HANDLER_MODULE_NAME = f'template_handler.{TEMPLATE_HANDLER_TYPE}'
-# CamelCase name of template_handler class in template_handler.template_handler.
+# do not change. Rename template_handler/sceptre_cdk_handler.py to template_handler/{TEMPLATE_HANDLER_TYPE}.py
+TEMPLATE_HANDLER_MODULE_NAME = f'sceptre_cdk_handler.{TEMPLATE_HANDLER_TYPE}'
+# CamelCase name of template_handler class in sceptre_cdk_handler.sceptre_cdk_handler.
 TEMPLATE_HANDLER_CLASS = 'CDK'
 # One line summary description
 TEMPLATE_HANDLER_DESCRIPTION = 'AWS CDK Template Handler'
@@ -26,7 +26,9 @@
 
 install_requirements = [
     "sceptre>=2.7",
-    "aws_cdk-lib>=2.51.1"
+    "aws_cdk-lib>=2.0,<3.0",
+    "cdk-bootstrapless-synthesizer>=2.0,<3.0",
+    "typing-extensions"
 ]
 
 test_requirements = [
diff --git a/template_handler/__init__.py b/template_handler/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/template_handler/cdk.py b/template_handler/cdk.py
deleted file mode 100644
index c7141ea..0000000
--- a/template_handler/cdk.py
+++ /dev/null
@@ -1,319 +0,0 @@
-import aws_cdk
-import importlib.machinery
-import importlib.util
-import logging
-import os
-import pathlib
-import shutil
-import subprocess
-import yaml
-from aws_cdk.cx_api import CloudAssembly
-from botocore.credentials import Credentials
-from pathlib import Path
-from sceptre import exceptions
-from sceptre.template_handlers import TemplateHandler
-from types import ModuleType
-from typing import Dict
-
-DEFAULT_CLASS_NAME = 'CdkStack'
-
-class CDK(TemplateHandler):
-    """
-    A template handler for AWS CDK templates. Using this will allow Sceptre to work with the AWS CDK to
-    build and package a CDK template and deploy it with Sceptre.
-    """
-
-    _internal_stack_name = 'CDKStack'
-
-    class PrefixLoggerAdapter(logging.LoggerAdapter):
-        """
-        Logger Adapter to specify a standard log entry prefix
-
-        This logger adapter expects to be passed in a dict-like object with a
-        'prefix' key, whose value is prefixed to the log message.
-        """
-        def process(self, msg, kwargs):
-            return f'{self.extra["prefix"]} - {msg}', kwargs
-
-    def schema(self):
-        """
-        Return a JSON schema of the properties that this template handler requires.
-
-        Reference: https://github.com/Julian/jsonschema
-        """
-        return {
-            "type": "object",
-            "properties": {
-                "path": {"type": "string"},
-                "context": {"type": "object"},
-                "class_name": {"type": "string"}
-            },
-            "required": [
-                "path"
-            ]
-        }
-
-    def _subprocess_run(self, cmd: str, env: Dict[str, str] = None) -> subprocess.CompletedProcess:
-        """
-        Run a command
-
-        Args:
-            cmd: str - The command to run
-            env: Dict[str, str] - The environment variables to pass to the command
-
-        Returns:
-            subprocess.CompletedProcess
-        """
-        result = subprocess.run(
-            cmd,
-            env=env,
-            shell=True,
-            capture_output=True
-        )
-
-        return result
-
-    def _get_envs(self) -> Dict[str, str]:
-        """
-        Obtains the environment variables to pass to the subprocess.
-
-        Sceptre can assume roles, profiles, etc... to connect to AWS for a given stack. This is
-        very useful. However, we need that SAME connection information to carry over to SAM when we
-        invoke it. The most precise way to do this is to use the same session credentials being used
-        by Sceptre for other stack operations. This method obtains those credentials and sets them
-        as environment variables that are passed to the subprocess and will, in turn, be used by
-        SAM CLI.
-
-        The environment variables dict created by this method will inherit all existing
-        environment variables in the current environment, but the AWS connection environment
-        variables will be overridden by the ones for this stack.
-
-        Returns:
-            The dictionary of environment variables.
-        """
-        envs = os.environ.copy()
-        # Set aws environment variables specific to whatever AWS configuration has been set on the
-        # stack's connection manager.
-        credentials: Credentials = self.connection_manager._get_session(
-            self.connection_manager.profile,
-            self.connection_manager.region,
-            self.connection_manager.iam_role
-        ).get_credentials()
-        envs.update(
-            AWS_ACCESS_KEY_ID=credentials.access_key,
-            AWS_SECRET_ACCESS_KEY=credentials.secret_key,
-        )
-
-        # There might not be a session token, so if there isn't one, make sure it doesn't exist in
-        # the envs being passed to the subprocess
-        if credentials.token is None:
-            envs.pop('AWS_SESSION_TOKEN', None)
-        else:
-            envs['AWS_SESSION_TOKEN'] = credentials.token
-
-        return envs
-
-    def _cmd_exists(self, cmd: str) -> bool:
-        """
-        Checks whether a specified CLI command exists.
-
-        Args:
-            cmd: str - The name of the CLI command to check
-
-        Returns:
-            bool
-        """
-
-        cmd_exists = shutil.which(cmd) is not None
-        self.logger.debug(f"command '{cmd}' exists: {cmd_exists}")
-        return cmd_exists
-
-    def _node_package_exists(self, package: str) -> bool:
-        """
-        Checks whether a specifie Node package exists either in the workspace or global scope.
-
-        Args:
-            package: str - The name of the node package to check
-
-        Returns:
-            bool
-        """
-
-        package_exists = False
-        workspace_result = self._subprocess_run(f'npm list {package}')
-        self.logger.debug(f"Workspace NPM package '{package}' exists: {not bool(workspace_result.returncode)}")
-
-        if workspace_result.returncode == 0:
-            package_exists = True
-        else:
-            global_result = self._subprocess_run(f'npm --global list {package}')
-            self.logger.debug(f"f'Global NPM package '{package}' exists: {not bool(global_result.returncode)}")
-
-            if global_result.returncode == 0:
-                package_exists = True
-
-        return package_exists
-
-    def _check_prerequisites(self) -> None:
-        """
-        Checks the command and Node package requirements for the handler.
-
-        Raises:
-            SceptreException: Command prerequisite not found
-            SceptreException: Node Package prerequisite not found
-        """
-
-        # Check Command Prerequisites
-        cmd_prerequisites = [
-            'node',
-            'npx'
-        ]
-        for cmd_prerequisite in cmd_prerequisites:
-            if not self._cmd_exists(cmd_prerequisite):
-                raise exceptions.SceptreException(f"{self.name} - Command prerequisite '{cmd_prerequisite}' not found")
-
-        # Check Node Package Prerequisites
-        node_prerequisites = [
-            'cdk-assets'
-        ]
-        for node_prerequisite in node_prerequisites:
-            if not self._node_package_exists(node_prerequisite):
-                raise exceptions.SceptreException(f"{self.name} - Node Package prerequisite '{node_prerequisite}' not found")
-
-    def _import_python_template_module(self, cdk_template_path: str) -> ModuleType:
-        """
-        Import the CDK Python template module.
-
-        Args:
-            cdk_template_path: str - The path of the CDK Template
-
-        Returns:
-            A ModuleType object containing the imported CDK Python module
-
-        Raises:
-            SceptreException: Template File not found
-            SceptreException: importlib general exception
-        """
-        template_path = str(pathlib.Path('templates', cdk_template_path).as_posix())
-        self.logger.debug(f'Importing CDK Python template module {template_path}')
-        template_module_name = pathlib.Path(template_path).stem
-        loader = importlib.machinery.SourceFileLoader(template_module_name, template_path)
-        spec = importlib.util.spec_from_loader(template_module_name, loader)
-        template_module = importlib.util.module_from_spec(spec)
-
-        try:
-            loader.exec_module(template_module)
-        except FileNotFoundError as err:
-            raise exceptions.SceptreException(f'{self.name} - Template not found: {err.filename}')
-        except Exception as err:
-            raise exceptions.SceptreException(f'{self.name} - {err}')
-
-        return template_module
-
-    def _cdk_synthesize(self, stack_name: str, template_module: ModuleType) -> CloudAssembly:
-        """
-        Synthesize the CDK App.
-
-        Args:
-            stack_name: str - The name of the CDK stack
-            template_module: ModuleType - The imported CDK Python module
-
-        Returns:
-            A CloudAssembly object for the CDK stack
-
-        Raises:
-            SceptreException: CDK Class not found
-        """
-        self.logger.debug(f'CDK synthesing CdkStack Class')
-        self.logger.debug(f'CDK Context: {self.cdk_context}')
-        app = aws_cdk.App(context=self.cdk_context)
-        try:
-            stack_class = getattr(template_module, self.cdk_class_name)
-        except Exception:
-            raise exceptions.SceptreException(f"{self.name} - CDK Class '{self.cdk_class_name}' not found.")
-
-        stack_class(app, stack_name, self.sceptre_user_data)
-        return app.synth()
-
-    def _publish_cdk_assets(self, app_synth: CloudAssembly) -> None:
-        """
-        Publishes the CDK Stack Assets.
-
-        Args:
-            app_synth: CloudAssembly - The Cloud Assembly of the CDK stack
-
-        Raises:
-            SceptreException: CDK Asset manifest artifact not found
-            SceptreException: Error publishing CDK assets
-        """
-        asset_artifacts = None
-
-        for artifacts in app_synth.artifacts:
-            if isinstance(artifacts, aws_cdk.cx_api.AssetManifestArtifact):
-                asset_artifacts = artifacts
-                break
-        if asset_artifacts is None:
-            raise exceptions.SceptreException(f'{self.name} - CDK Asset manifest artifact not found')
-        environment_variables = self._get_envs()
-        self.logger.info(f'Publishing CDK assets')
-        self.logger.debug(f'Assets manifest file: {asset_artifacts.file}')
-        cdk_assets_result = self._subprocess_run(
-            f'npx cdk-assets publish --path {asset_artifacts.file}',
-            env=environment_variables)
-        self.logger.info(f'{cdk_assets_result.stderr.decode()}')
-        if cdk_assets_result.returncode != 0:
-            raise exceptions.SceptreException(f'{self.name} - Error publishing CDK assets')
-
-    def handle(self) -> str:
-        """
-        Main Sceptre CDK Handler function
-
-        Returns:
-            str - The CDK synthesised CloudFormation template
-        """
-
-
-        self.logger = self.PrefixLoggerAdapter(self.logger, {'prefix': self.name})
-
-        self._check_prerequisites()
-
-        module = self._import_python_template_module(cdk_template_path=self.cdk_template_path)
-        app_synth = self._cdk_synthesize(stack_name=self._internal_stack_name, template_module=module)
-        self._publish_cdk_assets(app_synth=app_synth)
-
-        # Return synthesized template
-        template = app_synth.get_stack_by_name(self._internal_stack_name).template
-        return yaml.safe_dump(template)
-
-    @property
-    def cdk_template_path(self) -> Path:
-        """
-        CDK Template Path property handler.
-
-        Returns:
-            The specified CDK template path.
-        """
-
-        return self.arguments['path']
-
-    @property
-    def cdk_context(self) -> dict:
-        """
-        CDK Context property handler.
-
-        Returns:
-            The specified CDK context.
-        """
-
-        return self.arguments.get('context')
-
-    @property
-    def cdk_class_name(self) -> str:
-        """
-        CDK Class Name property handler.
-
-        Returns:
-            The specified CDK class name.
-        """
-
-        return self.arguments.get('class_name', DEFAULT_CLASS_NAME)


Test patch:
diff --git a/tests/assets/file_to_import.py b/tests/assets/file_to_import.py
new file mode 100644
index 0000000..6b10091
--- /dev/null
+++ b/tests/assets/file_to_import.py
@@ -0,0 +1,3 @@
+
+class MyFancyClassToImport:
+    attribute = "Success!"
diff --git a/tests/test_cdk.py b/tests/test_cdk.py
new file mode 100644
index 0000000..1aeaef8
--- /dev/null
+++ b/tests/test_cdk.py
@@ -0,0 +1,211 @@
+from pathlib import Path
+from unittest import TestCase
+from unittest.mock import Mock, create_autospec
+
+import yaml
+from sceptre.connection_manager import ConnectionManager
+from sceptre.exceptions import SceptreException
+
+from sceptre_cdk_handler.cdk import CDK, DEFAULT_CLASS_NAME, QUALIFIER_CONTEXT_KEY
+from sceptre_cdk_handler.cdk_builder import BootstrappedCdkBuilder, BootstraplessCdkBuilder
+from sceptre_cdk_handler.class_importer import ClassImporter
+from sceptre_cdk_handler.command_checker import CommandChecker
+
+
+class TestCDK(TestCase):
+    def setUp(self):
+        self.name = "CDK"
+        self.connection_manager = Mock(ConnectionManager)
+        self.arguments = {
+            'path': 'my/template/path.py',
+            'deployment_type': 'bootstrapped'
+        }
+        self.sceptre_user_data = {
+            'key': 'value'
+        }
+        self.stack_group_config = {
+            'project_path': 'root_sceptre_dir'
+        }
+        self.importer_class = create_autospec(ClassImporter)
+        self.template_dict = {'Resources': {}}
+        self.bootstrapped_builder_class = create_autospec(BootstrappedCdkBuilder)
+        self.bootstrapless_builder_class = create_autospec(BootstraplessCdkBuilder)
+        self.bootstrapped_builder_class.return_value.build_template.return_value = self.template_dict
+        self.bootstrapless_builder_class.return_value.build_template.return_value = self.template_dict
+        self.command_checker_class = create_autospec(CommandChecker)
+        self.command_checker_class.return_value.cmd_exists.side_effect = self.cmd_exists
+        self.command_checker_class.return_value.node_package_exists.side_effect = self.node_package_exists
+
+        self.commands = {
+            'node': True,
+            'npx': True,
+        }
+        self.node_packages = {
+            'cdk-assets': True
+        }
+
+    def cmd_exists(self, cmd_name):
+        return self.commands.get(cmd_name, False)
+
+    def node_package_exists(self, package_name):
+        return self.node_packages.get(package_name, False)
+
+    def get_handler(self) -> CDK:
+        handler = CDK(
+            self.name,
+            self.arguments,
+            self.sceptre_user_data,
+            self.connection_manager,
+            self.stack_group_config,
+            importer_class=self.importer_class,
+            bootstrapped_cdk_builder_class=self.bootstrapped_builder_class,
+            bootstrapless_cdk_builder_class=self.bootstrapless_builder_class,
+            command_checker_class=self.command_checker_class
+        )
+        return handler
+
+    def validate_and_handle(self):
+        handler = self.get_handler()
+        handler.validate()
+        return handler.handle()
+
+    def test_handle__class_name_as_argument__imports_named_class_from_specified_template_path(self):
+        self.arguments['class_name'] = "MyFancyClass"
+        self.validate_and_handle()
+
+        expected_template_path = Path(
+            self.stack_group_config['project_path'],
+            'templates',
+            self.arguments['path']
+        )
+        self.importer_class.return_value.import_class.assert_any_call(
+            expected_template_path,
+            'MyFancyClass'
+        )
+
+    def test_handle__no_class_name_argument__imports_default_class_name_from_template_path(self):
+        self.validate_and_handle()
+
+        expected_template_path = Path(
+            self.stack_group_config['project_path'],
+            'templates',
+            self.arguments['path']
+        )
+        self.importer_class.return_value.import_class.assert_any_call(
+            expected_template_path,
+            DEFAULT_CLASS_NAME
+        )
+
+    def test_handle__bootstrapped__bootstrap_qualifier_set__no_context__builds_template_with_qualifier_in_context(self):
+        self.arguments['deployment_type'] = 'bootstrapped'
+        self.arguments['bootstrap_qualifier'] = qualifier = 'blardyblahr'
+        self.validate_and_handle()
+        self.bootstrapped_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager)
+        expected_context = {
+            QUALIFIER_CONTEXT_KEY: qualifier
+        }
+        self.bootstrapped_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            expected_context,
+            self.sceptre_user_data
+        )
+
+    def test_handle__bootstrapped__bootstrap_qualifier_set__has_context__builds_template_with_qualifier_in_context(self):
+        self.arguments['deployment_type'] = 'bootstrapped'
+        self.arguments['bootstrap_qualifier'] = qualifier = 'blardyblahr'
+        self.arguments['context'] = context = {
+            'something': 'else'
+        }
+        expected_context = context.copy()
+        self.validate_and_handle()
+        self.bootstrapped_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager)
+
+        expected_context[QUALIFIER_CONTEXT_KEY] = qualifier
+        self.bootstrapped_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            expected_context,
+            self.sceptre_user_data
+        )
+
+    def test_handle__bootstrapped__bootstrap_qualifier_in_context__builds_template_with_qualifier_in_context(self):
+        self.arguments['deployment_type'] = 'bootstrapped'
+        self.arguments['context'] = context = {
+            'something': 'else',
+            QUALIFIER_CONTEXT_KEY: 'blardyblahr'
+        }
+        expected_context = context.copy()
+        self.validate_and_handle()
+        self.bootstrapped_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager)
+
+        self.bootstrapped_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            expected_context,
+            self.sceptre_user_data
+        )
+
+    def test_handle__bootstrapped__no_qualifier_but_has_context__builds_template_with_context(self):
+        self.arguments['context'] = context = {'key': 'value'}
+        self.validate_and_handle()
+        self.bootstrapped_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager)
+        self.bootstrapped_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            context,
+            self.sceptre_user_data
+        )
+
+    def test_handle__bootstrapped__no_qualifier_or_context__builds_template_with_no_context(self):
+        self.arguments['deployment_type'] = 'bootstrapped'
+        self.validate_and_handle()
+        self.bootstrapped_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager)
+        self.bootstrapped_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            None,
+            self.sceptre_user_data
+        )
+
+    def test_handle_bootstrapless__no_bootstrapless_config__builds_template_with_empty_bootstrapless_config(self):
+        self.arguments['deployment_type'] = 'bootstrapless'
+        self.arguments['context'] = context = {'something': 'else'}
+        self.validate_and_handle()
+        self.bootstrapless_builder_class.assert_any_call(self.get_handler().logger, self.connection_manager, {})
+        self.bootstrapless_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            context,
+            self.sceptre_user_data
+        )
+
+    def test_handle_bootstrapless__bootstrapless_config__builds_template_with_bootstrapless_config(self):
+        self.arguments['deployment_type'] = 'bootstrapless'
+        self.arguments['context'] = context = {'something': 'else'}
+        self.arguments['bootstrapless_config'] = config = {
+            'file_asset_bucket_name': 'my_bucket'
+        }
+        self.validate_and_handle()
+        self.bootstrapless_builder_class.assert_any_call(
+            self.get_handler().logger,
+            self.connection_manager,
+            config
+        )
+        self.bootstrapless_builder_class.return_value.build_template.assert_any_call(
+            self.importer_class.return_value.import_class.return_value,
+            context,
+            self.sceptre_user_data
+        )
+
+    def test_handle__returns_dumped_yaml_template(self):
+        result = self.validate_and_handle()
+        expected = yaml.dump(self.template_dict)
+        self.assertEqual(expected, result)
+
+    def test_validate__command_doesnt_exist__raises_sceptre_exception(self):
+        for command in ['node', 'npx']:
+            with self.subTest(command):
+                self.setUp()
+                self.commands.pop(command)
+                with self.assertRaises(SceptreException):
+                    self.get_handler().validate()
+
+    def test_validate__cdk_assets_doesnt_exist__raises(self):
+        self.node_packages.pop('cdk-assets')
+        with self.assertRaises(SceptreException):
+            self.get_handler().validate()
diff --git a/tests/test_cdk_builder.py b/tests/test_cdk_builder.py
new file mode 100644
index 0000000..bbaf63a
--- /dev/null
+++ b/tests/test_cdk_builder.py
@@ -0,0 +1,261 @@
+import logging
+import subprocess
+import sys
+from unittest import TestCase
+from unittest.mock import Mock, create_autospec
+
+import aws_cdk
+from aws_cdk.cx_api import CloudAssembly
+from cdk_bootstrapless_synthesizer import BootstraplessStackSynthesizer
+from sceptre.connection_manager import ConnectionManager
+from sceptre.exceptions import SceptreException, TemplateHandlerArgumentsInvalidError
+
+from sceptre_cdk_handler.cdk_builder import (
+    BootstrappedCdkBuilder,
+    BootstraplessCdkBuilder,
+    SceptreCdkStack,
+    CdkBuilder
+)
+
+
+class TestBootstrappedCdkBuilder(TestCase):
+    def setUp(self):
+        self.logger = Mock(logging.Logger)
+        self.profile = 'my_fancy_profile'
+        self.region = 'us-west-2'
+        self.iam_role = 'arn:aws:something:or:another:role'
+        self.credentials = Mock(
+            access_key="key",
+            secret_key="secret",
+            token=None
+        )
+
+        self.connection_manager = Mock(
+            ConnectionManager,
+            **{
+                'profile': self.profile,
+                'region': self.region,
+                'iam_role': self.iam_role,
+                '_get_session.return_value.get_credentials.return_value': self.credentials
+            }
+        )
+        self.subprocess_run = create_autospec(subprocess.run)
+        self.app_class = create_autospec(aws_cdk.App)
+        self.assembly = Mock(CloudAssembly)
+        self.app_class.return_value.synth.return_value = self.assembly
+        self.manifest = Mock(**{
+            'spec': aws_cdk.cx_api.AssetManifestArtifact,
+            'file': "asset/file/path",
+            'contents.docker_images': {
+                "hashhashhash": {"blah": "blah"}
+            },
+            'contents.files': {
+                "hashyhashhash": {"blah": "blah"},
+                "CDKStack.template.json": {"blah": "blah"}
+            }
+        })
+
+        self.assembly.artifacts = [
+            Mock(name="irrelevant_artifact"),
+            self.manifest
+        ]
+        self.environment_variables = {
+            "PATH": "blah:blah:blah",
+            "AWS_ACCESS_KEY_ID": "old key",
+            "AWS_SECRET_ACCESS_KEY": "old secret",
+            "AWS_SESSION_TOKEN": "old token"
+        }
+
+        self.builder = BootstrappedCdkBuilder(
+            self.logger,
+            self.connection_manager,
+            subprocess_run=self.subprocess_run,
+            app_class=self.app_class,
+            environment_variables=self.environment_variables
+        )
+
+        self.stack_class = create_autospec(SceptreCdkStack)
+        self.stack_class.__name__ = "MyFancyStack"
+        self.context = {'hello': 'you'}
+        self.sceptre_user_data = {'user': 'data'}
+
+    def test_build_template__instantiates_app_with_context(self):
+        self.build()
+        self.app_class.assert_any_call(context=self.context)
+
+    def build(self):
+        return self.builder.build_template(
+            self.stack_class,
+            self.context,
+            self.sceptre_user_data
+        )
+
+    def test_build_template__adds_stack_instance_to_app_with_user_data(self):
+        self.build()
+
+        self.stack_class.assert_any_call(
+            self.app_class.return_value,
+            CdkBuilder.STACK_LOGICAL_ID,
+            self.sceptre_user_data
+        )
+
+    def test_build_template__synthesized_assembly_has_no_manifest_artifact__raises_sceptre_exception(self):
+        del self.assembly.artifacts[1]
+        with self.assertRaises(SceptreException):
+            self.build()
+
+    def test_build_template__synthesized_assembly_has_only_template_asset__does_not_publish_assets(self):
+        self.manifest.contents.docker_images.clear()
+        self.manifest.contents.files.pop('hashyhashhash')
+        self.build()
+        self.subprocess_run.assert_not_called()
+
+    def test_build_template__no_session_token__runs_cdk_assets_publish_on_the_asset_artifacts_file_with_correct_envs(self):
+        self.credentials.token = None
+        self.build()
+        expected_command = 'npx cdk-assets -v publish --path asset/file/path'
+        expected_envs = {
+            **self.environment_variables,
+            **{
+                "AWS_ACCESS_KEY_ID": self.credentials.access_key,
+                "AWS_SECRET_ACCESS_KEY": self.credentials.secret_key,
+                "AWS_DEFAULT_REGION": self.connection_manager.region,
+                "CDK_DEFAULT_REGION": self.connection_manager.region,
+                "AWS_REGION": self.connection_manager.region
+            }
+        }
+        del expected_envs['AWS_SESSION_TOKEN']
+        self.subprocess_run.assert_any_call(
+            expected_command,
+            env=expected_envs,
+            shell=True,
+            stdout=sys.stderr,
+            check=True
+        )
+
+    def test_build_template__aws_session_token_in_envs__does_not_publish_assets_with_profile_specified(self):
+        self.environment_variables['AWS_PROFILE'] = 'dont_use_me'
+        called = False
+
+        def fake_subprocess(*args, env, **kwargs):
+            nonlocal called
+            self.assertNotIn('AWS_PROFILE', env)
+            called = True
+
+        self.subprocess_run.side_effect = fake_subprocess
+
+        self.build()
+        self.assertTrue(called)
+
+    def test_build_template__with_session_token__runs_cdk_assets_publish_on_the_asset_artifacts_file_with_correct_envs(self):
+        self.credentials.token = "special session token"
+        self.build()
+        expected_command = 'npx cdk-assets -v publish --path asset/file/path'
+        expected_envs = {
+            **self.environment_variables,
+            **{
+                "AWS_ACCESS_KEY_ID": self.credentials.access_key,
+                "AWS_SECRET_ACCESS_KEY": self.credentials.secret_key,
+                "AWS_SESSION_TOKEN": self.credentials.token,
+                "AWS_DEFAULT_REGION": self.connection_manager.region,
+                "CDK_DEFAULT_REGION": self.connection_manager.region,
+                "AWS_REGION": self.connection_manager.region
+            }
+        }
+        self.subprocess_run.assert_any_call(
+            expected_command,
+            env=expected_envs,
+            shell=True,
+            stdout=sys.stderr,
+            check=True
+        )
+
+    def test_build_template__returns_template_from_cloud_assembly_for_stack(self):
+        expected_template = {'Resources': {}}
+
+        def get_stack_by_name(stack_id):
+            self.assertEqual(CdkBuilder.STACK_LOGICAL_ID, stack_id)
+            return Mock(template=expected_template)
+
+        self.assembly.get_stack_by_name = get_stack_by_name
+
+        result = self.build()
+        self.assertEqual(expected_template, result)
+
+
+class TestBootstraplessCdkBuilder(TestCase):
+    def setUp(self):
+        self.logger = Mock(logging.Logger)
+        self.profile = 'my_fancy_profile'
+        self.region = 'us-west-2'
+        self.iam_role = 'arn:aws:something:or:another:role'
+        self.credentials = Mock(
+            access_key="key",
+            secret_key="secret",
+            token=None
+        )
+
+        self.connection_manager = Mock(
+            ConnectionManager,
+            **{
+                'profile': self.profile,
+                'region': self.region,
+                'iam_role': self.iam_role,
+                '_get_session.return_value.get_credentials.return_value': self.credentials
+            }
+        )
+        self.subprocess_run = create_autospec(subprocess.run)
+        self.app_class = create_autospec(aws_cdk.App)
+        self.assembly = Mock(CloudAssembly)
+        self.app_class.return_value.synth.return_value = self.assembly
+        self.assembly.artifacts = [
+            Mock(name="irrelevant_artifact"),
+            Mock(spec=aws_cdk.cx_api.AssetManifestArtifact, file="asset/file/path")
+        ]
+        self.environment_variables = {
+            "PATH": "blah:blah:blah",
+            "AWS_ACCESS_KEY_ID": "old key",
+            "AWS_SECRET_ACCESS_KEY": "old secret",
+            "AWS_SESSION_TOKEN": "old token"
+        }
+        self.synthesizer_config = {
+            'file_asset_bucket_name': 'my_bucket'
+        }
+        self.synthesizer_class = create_autospec(BootstraplessStackSynthesizer)
+        self.builder = BootstraplessCdkBuilder(
+            self.logger,
+            self.connection_manager,
+            self.synthesizer_config,
+            subprocess_run=self.subprocess_run,
+            app_class=self.app_class,
+            environment_variables=self.environment_variables,
+            synthesizer_class=self.synthesizer_class
+        )
+
+        self.stack_class = create_autospec(SceptreCdkStack)
+        self.stack_class.__name__ = "MyFancyBootstraplessStack"
+        self.context = {'hello': 'you'}
+        self.sceptre_user_data = {'user': 'data'}
+
+    def test_build_template__no_synthesizer_config__instantiates_synthesizer_with_no_kwargs(self):
+        self.synthesizer_config.clear()
+        self.builder.build_template(self.stack_class, self.context, self.sceptre_user_data)
+        self.synthesizer_class.assert_any_call(**self.synthesizer_config)
+
+    def test_build_template__instantiates_synthesizer_with_synthesizer_config_kwargs(self):
+        self.builder.build_template(self.stack_class, self.context, self.sceptre_user_data)
+        self.synthesizer_class.assert_any_call(**self.synthesizer_config)
+
+    def test_build_template__invalid_synthesizer_arguments__raises_template_handler_arguments_invalid_error(self):
+        self.synthesizer_config['bad'] = 'not-recognized'
+        with self.assertRaises(TemplateHandlerArgumentsInvalidError):
+            self.builder.build_template(self.stack_class, self.context, self.sceptre_user_data)
+
+    def test_build_template__instantiates_stack_with_synthesizer(self):
+        self.builder.build_template(self.stack_class, self.context, self.sceptre_user_data)
+        self.stack_class.assert_any_call(
+            self.app_class.return_value,
+            CdkBuilder.STACK_LOGICAL_ID,
+            self.sceptre_user_data,
+            synthesizer=self.synthesizer_class.return_value
+        )
diff --git a/tests/test_class_importer.py b/tests/test_class_importer.py
new file mode 100644
index 0000000..6799f53
--- /dev/null
+++ b/tests/test_class_importer.py
@@ -0,0 +1,24 @@
+from pathlib import Path
+from unittest import TestCase
+
+from sceptre.exceptions import SceptreException
+
+from sceptre_cdk_handler.class_importer import ClassImporter
+
+
+class TestClassImporter(TestCase):
+    def test_import_class__imports_class_from_referenced_file(self):
+        filepath = Path(__file__).parent / 'assets' / 'file_to_import.py'
+        class_name = "MyFancyClassToImport"
+        importer = ClassImporter()
+        result = importer.import_class(filepath, class_name)
+        self.assertEqual(
+            'Success!',
+            result.attribute
+        )
+
+    def test_import_class__named_class_isnt_on_module__raises_sceptre_exception(self):
+        filepath = Path(__file__).parent / 'assets' / 'file_to_import.py'
+        importer = ClassImporter()
+        with self.assertRaises(SceptreException):
+            importer.import_class(filepath, "CantFindMe")
diff --git a/tests/test_command_checker.py b/tests/test_command_checker.py
new file mode 100644
index 0000000..2f8957a
--- /dev/null
+++ b/tests/test_command_checker.py
@@ -0,0 +1,65 @@
+import shutil
+import subprocess
+from logging import Logger
+from unittest import TestCase
+from unittest.mock import create_autospec, Mock
+
+from sceptre_cdk_handler.command_checker import CommandChecker
+
+
+class TestCommandChecker(TestCase):
+
+    def setUp(self):
+        self.which_func = create_autospec(shutil.which, side_effect=self.which)
+        self.subprocess_run_func = create_autospec(subprocess.run, side_effect=self.subprocess_run)
+        self.logger = Mock(Logger)
+
+        self.commands = {
+            "exists": "/path/to/executable"
+        }
+
+        self.local_npm_packages = {
+            'local': True
+        }
+
+        self.global_npm_packages = {
+            'global': True
+        }
+
+        self.checker = CommandChecker(
+            self.logger,
+            which_func=self.which_func,
+            subprocess_run=self.subprocess_run_func
+        )
+
+    def which(self, cmd):
+        return self.commands.get(cmd)
+
+    def subprocess_run(self, cmd, *, shell):
+        self.assertTrue(shell)
+        command_segments = cmd.split(' ')
+        if command_segments[:2] == ['npm', 'list']:
+            package_name = command_segments[2]
+            found = self.local_npm_packages.get(package_name, False)
+        elif command_segments[:3] == ['npm', '--global', 'list']:
+            package_name = command_segments[3]
+            found = self.global_npm_packages.get(package_name, False)
+        else:
+            raise AssertionError("Unexpected command!")
+
+        return Mock(returncode=0 if found else 1)
+
+    def test_cmd_exists__which_returns_none__returns_false(self):
+        self.assertFalse(self.checker.cmd_exists('nonexistant'))
+
+    def test_cmd_exists__which_returns_value__returns_true(self):
+        self.assertTrue(self.checker.cmd_exists('exists'))
+
+    def test_node_package_exists__exists_locally__returns_true(self):
+        self.assertTrue(self.checker.node_package_exists('local'))
+
+    def test_node_package_exists__exists_globally__returns_true(self):
+        self.assertTrue(self.checker.node_package_exists('global'))
+
+    def test_node_package_exists__does_not_exist__returns_false(self):
+        self.assertFalse(self.checker.node_package_exists('nonexistant'))
diff --git a/tests/test_handler.py b/tests/test_handler.py
deleted file mode 100644
index 55adb88..0000000
--- a/tests/test_handler.py
+++ /dev/null
@@ -1,445 +0,0 @@
-import importlib.machinery
-import importlib.util
-import logging
-import os
-import shutil
-import subprocess
-import textwrap
-import pytest
-from aws_cdk.cx_api import CloudAssembly
-from botocore.credentials import Credentials
-from boto3.session import Session
-from sceptre import exceptions
-from template_handler.cdk import CDK
-from unittest.mock import Mock, MagicMock
-from sceptre.connection_manager import ConnectionManager
-
-name = 'dev/cdk'
-sceptre_user_data = {'object_name': 'object-key.txt'}
-stack_group_config = {'project_code': 'sceptre'}
-arguments = {
-    'path': 'CDK/s3.py'
-}
-aws_profile = 'profile1'
-aws_region = 'eu-west-1'
-aws_iam_role = 'role1'
-
-aws_access_key = '123'
-aws_secret_key = '456'
-aws_token = '789'
-
-connection_manager = MagicMock(
-    spec=ConnectionManager
-)
-
-
-def test_schema():
-    """
-    Test schema function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config)
-
-    # Act
-    result = cdk_handler.schema()
-
-    # Assert
-    assert result['type'] == 'object'
-    assert result['properties'] is not None
-    assert result['required'] is not None
-
-
-def test_get_envs():
-    """
-    Test _get_envs function
-    """
-    # Arrange
-    credentials = Credentials(
-        access_key=aws_access_key,
-        secret_key=aws_secret_key
-    )
-
-    connection_manager = MagicMock(
-        spec=ConnectionManager,
-        profile=aws_profile,
-        region=aws_region,
-        iam_role=aws_iam_role
-    )
-
-    connection_manager._get_session = MagicMock(
-        spec_set=Session
-    )
-    connection_manager._get_session.return_value.get_credentials.return_value = credentials
-
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    # Act
-    result = cdk_handler._get_envs()
-
-    # Assert
-    assert result.get('AWS_ACCESS_KEY_ID') == aws_access_key
-    assert result.get('AWS_SECRET_ACCESS_KEY') == aws_secret_key
-    assert result.get('AWS_SESSION_TOKEN') is None
-
-
-def test_get_envs_with_session_token():
-    """
-    Test _get_envs function
-    """
-    # Arrange
-    credentials = Credentials(
-        access_key=aws_access_key,
-        secret_key=aws_secret_key,
-        token=aws_token
-    )
-
-    connection_manager = MagicMock(
-        spec=ConnectionManager,
-        profile=aws_profile,
-        region=aws_region,
-        iam_role=aws_iam_role
-    )
-
-    connection_manager._get_session = MagicMock(
-        spec_set=Session
-    )
-    connection_manager._get_session.return_value.get_credentials.return_value = credentials
-
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    # Act
-    result = cdk_handler._get_envs()
-
-    # Assert
-    assert result.get('AWS_ACCESS_KEY_ID') == aws_access_key
-    assert result.get('AWS_SECRET_ACCESS_KEY') == aws_secret_key
-    assert result.get('AWS_SESSION_TOKEN') == aws_token
-
-
-def test_cmd_exists():
-    """
-    Test _cmd_exists function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    shutil.which = MagicMock(
-        return_value='/bin/mock_cmd'
-    )
-
-    # Act
-    result = cdk_handler._cmd_exists('mock_cmd')
-
-    # Assert
-    assert result == True
-    shutil.which.assert_called_once()
-
-
-def test_cmd_exists_no_command():
-    """
-    Test _cmd_exists function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    shutil.which = MagicMock(
-        return_value=None
-    )
-
-    # Act
-    result = cdk_handler._cmd_exists('mock_cmd')
-
-    # Assert
-    assert result == False
-    shutil.which.assert_called_once()
-
-
-def test_node_package_exists():
-    """
-    Test _node_package_exists function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    success = 0
-    failure = 1
-    workspace_node_package = 'workspace'
-    global_node_package = 'global'
-    node_package_not_exists = 'notexist'
-
-    def run_side_effect(cmd, **kwargs):
-        completed_process = MagicMock(
-            spec=subprocess.CompletedProcess)
-        if cmd == f'npm list {workspace_node_package}':
-            completed_process.returncode = success
-            return completed_process
-        elif cmd == f'npm --global list {global_node_package}':
-            completed_process.returncode = success
-            return completed_process
-        else:
-            completed_process.returncode = failure
-            return completed_process
-
-    cdk_handler._subprocess_run = MagicMock(
-        side_effect=run_side_effect
-    )
-
-    # Act
-    result = cdk_handler._node_package_exists(workspace_node_package)
-
-    # Assert
-    assert result == True
-
-    # Act
-    result = cdk_handler._node_package_exists(global_node_package)
-
-    # Assert
-    assert result == True
-
-    # Act
-    result = cdk_handler._node_package_exists(node_package_not_exists)
-
-    # Assert
-    assert result == False
-
-
-def test_check_prerequisities():
-    """
-    Test _check_prerequisities function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    cdk_handler._cmd_exists = MagicMock(
-        return_value=True
-    )
-    cdk_handler._node_package_exists = MagicMock(
-        return_value=True
-    )
-
-    cdk_handler._check_prerequisites()
-
-    cdk_handler._cmd_exists = MagicMock(
-        return_value=False
-    )
-    cdk_handler._node_package_exists = MagicMock(
-        return_value=True
-    )
-
-    # Act
-    with pytest.raises(exceptions.SceptreException) as excep:
-        cdk_handler._check_prerequisites()
-
-        # Assert
-        assert "Command prerequisite node not found" in excep
-
-    # Arrange
-    cdk_handler._cmd_exists = MagicMock(
-        return_value=True
-    )
-    cdk_handler._node_package_exists = MagicMock(
-        return_value=False
-    )
-
-    # Act
-    with pytest.raises(exceptions.SceptreException) as excep:
-        cdk_handler._check_prerequisites()
-
-        # Assert
-        assert "Node Package prerequisite 'sdk-assets' not found" in excep
-
-
-def test_import_python_template_module():
-    """
-    Test _import_python_template_module function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    cwd = os.getcwd()
-    os.chdir('sceptre-example')
-    cdk_template_path = 'CDK/s3.py'
-
-    cdk_handler._import_python_template_module(cdk_template_path=cdk_template_path)
-
-    cdk_template_path = 'CDK/non-existent-template.py'
-
-    # Act
-    with pytest.raises(exceptions.SceptreException) as excep:
-        cdk_handler._import_python_template_module(cdk_template_path=cdk_template_path)
-
-        # Assert
-        assert f"Template not found: {cdk_template_path}" in excep
-
-    # Cleanup
-    os.chdir(cwd)
-
-
-def test_cdk_synthesize():
-    """
-    Test _cdk_synthesize function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    cwd = os.getcwd()
-    os.chdir('sceptre-example')
-    cdk_template_path = 'CDK/s3.py'
-    template_module = cdk_handler._import_python_template_module(cdk_template_path=cdk_template_path)
-    stack_name = 'CDKStack'
-
-    # Act
-    result = cdk_handler._cdk_synthesize(stack_name, template_module=template_module)
-
-    # Assert
-    assert isinstance(result, CloudAssembly) == True
-
-    # Cleanup
-    os.chdir(cwd)
-
-
-def test_publish_cdk_assets():
-    """
-    Test _publish_cdk_assets function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    cwd = os.getcwd()
-    os.chdir('sceptre-example')
-    cdk_template_path = 'CDK/s3.py'
-    module = cdk_handler._import_python_template_module(cdk_template_path=cdk_template_path)
-    stack_name = 'CDKStack'
-    app_synth = cdk_handler._cdk_synthesize(stack_name, template_module=module)
-
-    success = 0
-    failure = 1
-    completed_process = MagicMock(
-        spec=subprocess.CompletedProcess,
-        returncode=success,
-        stderr=MagicMock(
-            decode=MagicMock(
-                return_value=''
-            )
-        )
-    )
-
-    cdk_handler._subprocess_run = MagicMock(
-        return_value=completed_process
-    )
-
-    cdk_handler._get_envs = MagicMock()
-
-    # Act
-    cdk_handler._publish_cdk_assets(app_synth=app_synth)
-
-    # Assert
-    cdk_handler._subprocess_run.assert_called_once()
-
-    # Cleanup
-    os.chdir(cwd)
-
-
-def test_handler():
-    """
-    Test handler function
-    """
-    # Arrange
-    cdk_handler = CDK(
-        name,
-        arguments=arguments,
-        sceptre_user_data=sceptre_user_data,
-        connection_manager=connection_manager,
-        stack_group_config=stack_group_config
-    )
-
-    module = MagicMock()
-    app_synth = MagicMock()
-    app_synth.get_stack_by_name.return_value.template = {
-        'Resources': {
-            'Resource1': {
-                'Type': 'AWS::S3::Bucket'
-            }
-        }
-    }
-    template_result = textwrap.dedent(
-        """\
-        Resources:
-          Resource1:
-            Type: AWS::S3::Bucket
-        """)
-
-    cdk_handler._check_prerequisites = MagicMock()
-    cdk_handler._import_python_template_module = MagicMock(return_value=module)
-    cdk_handler._cdk_synthesize = MagicMock(return_value=app_synth)
-    cdk_handler._publish_cdk_assets = MagicMock()
-
-    # Act
-    result = cdk_handler.handle()
-
-    # Assert
-    assert result == template_result
-    cdk_handler._check_prerequisites.assert_called_once()
-    cdk_handler._import_python_template_module.assert_called_once_with(cdk_template_path=cdk_handler.cdk_template_path)
-    cdk_handler._cdk_synthesize.assert_called_once_with(stack_name=cdk_handler._internal_stack_name, template_module=module)
-    cdk_handler._publish_cdk_assets.assert_called_once_with(app_synth=app_synth)
-    app_synth.get_stack_by_name.assert_called_once_with(cdk_handler._internal_stack_name)