Customize constructs from the AWS Construct Library - AWS Cloud Development Kit (AWS CDK) v2

This is the AWS CDK v2 Developer Guide. The older CDK v1 entered maintenance on June 1, 2022 and ended support on June 1, 2023.

Customize constructs from the AWS Construct Library

Customize constructs from the AWS Construct Library through escape hatches, raw overrides, and custom resources.

Use escape hatches

The AWS Construct Library provides constructs of varying levels of abstraction.

At the highest level, your AWS CDK application and the stacks in it are themselves abstractions of your entire cloud infrastructure, or significant chunks of it. They can be parameterized to deploy them in different environments or for different needs.

Abstractions are powerful tools for designing and implementing cloud applications. The AWS CDK gives you the power not only to build with its abstractions, but also to create new abstractions. Using the existing open-source L2 and L3 constructs as guidance, you can build your own L2 and L3 constructs to reflect your own organization's best practices and opinions.

No abstraction is perfect, and even good abstractions cannot cover every possible use case. During development, you may find a construct that almost fits your needs, requiring a small or large customization.

For this reason, the AWS CDK provides ways to break out of the construct model. This includes moving to a lower-level abstraction or to a different model entirely. Escape hatches let you escape the AWS CDK paradigm and customize it in ways that suit your needs. Then, you can wrap your changes in a new construct to abstract away the underlying complexity and provide a clean API for other developers.

The following are examples of situations where you can use escape hatches:

  • An AWS service feature is available through AWS CloudFormation, but there are no L2 constructs for it.

  • An AWS service feature is available through AWS CloudFormation, and there are L2 constructs for the service, but these don't yet expose the feature. Because L2 constructs are curated by the CDK team, they may not be immediately available for new features.

  • The feature is not yet available through AWS CloudFormation at all.

    To determine whether a feature is available through AWS CloudFormation, see AWS Resource and Property Types Reference.

Develop escape hatches for L1 constructs

If L2 constructs are not available for the service, you can use the automatically generated L1 constructs. These resources can be recognized by their name starting with Cfn, such as CfnBucket or CfnRole. You instantiate them exactly as you would use the equivalent AWS CloudFormation resource.

For example, to instantiate a low-level Amazon S3 bucket L1 with analytics enabled, you would write something like the following.

TypeScript
new s3.CfnBucket(this, 'amzn-s3-demo-bucket', { analyticsConfigurations: [ { id: 'Config', // ... } ] });
JavaScript
new s3.CfnBucket(this, 'amzn-s3-demo-bucket', { analyticsConfigurations: [ { id: 'Config' // ... } ] });
Python
s3.CfnBucket(self, "amzn-s3-demo-bucket", analytics_configurations: [ dict(id="Config", # ... ) ] )
Java
CfnBucket.Builder.create(this, "amzn-s3-demo-bucket") .analyticsConfigurations(Arrays.asList(java.util.Map.of( // Java 9 or later "id", "Config", // ... ))).build();
C#
new CfnBucket(this, 'amzn-s3-demo-bucket', new CfnBucketProps { AnalyticsConfigurations = new Dictionary<string, string> { ["id"] = "Config", // ... } });

There might be rare cases where you want to define a resource that doesn't have a corresponding CfnXxx class. This could be a new resource type that hasn't yet been published in the AWS CloudFormation resource specification. In cases like this, you can instantiate the cdk.CfnResource directly and specify the resource type and properties. This is shown in the following example.

TypeScript
new cdk.CfnResource(this, 'amzn-s3-demo-bucket', { type: 'AWS::S3::Bucket', properties: { // Note the PascalCase here! These are CloudFormation identifiers. AnalyticsConfigurations: [ { Id: 'Config', // ... } ] } });
JavaScript
new cdk.CfnResource(this, 'amzn-s3-demo-bucket', { type: 'AWS::S3::Bucket', properties: { // Note the PascalCase here! These are CloudFormation identifiers. AnalyticsConfigurations: [ { Id: 'Config' // ... } ] } });
Python
cdk.CfnResource(self, 'amzn-s3-demo-bucket', type="AWS::S3::Bucket", properties=dict( # Note the PascalCase here! These are CloudFormation identifiers. "AnalyticsConfigurations": [ { "Id": "Config", # ... } ] } )
Java
CfnResource.Builder.create(this, "amzn-s3-demo-bucket") .type("AWS::S3::Bucket") .properties(java.util.Map.of( // Map.of requires Java 9 or later // Note the PascalCase here! These are CloudFormation identifiers "AnalyticsConfigurations", Arrays.asList( java.util.Map.of("Id", "Config", // ... )))) .build();
C#
new CfnResource(this, "amzn-s3-demo-bucket", new CfnResourceProps { Type = "AWS::S3::Bucket", Properties = new Dictionary<string, object> { // Note the PascalCase here! These are CloudFormation identifiers ["AnalyticsConfigurations"] = new Dictionary<string, string>[] { new Dictionary<string, string> { ["Id"] = "Config" } } } });

Develop escape hatches for L2 constructs

If an L2 construct is missing a feature or you're trying to work around an issue, you can modify the L1 construct that's encapsulated by the L2 construct.

All L2 constructs contain within them the corresponding L1 construct. For example, the high-level Bucket construct wraps the low-level CfnBucket construct. Because the CfnBucket corresponds directly to the AWS CloudFormation resource, it exposes all features that are available through AWS CloudFormation.

The basic approach to get access to the L1 construct is to use construct.node.defaultChild (Python: default_child), cast it to the right type (if necessary), and modify its properties. Again, let's take the example of a Bucket.

TypeScript
// Get the CloudFormation resource const cfnBucket = bucket.node.defaultChild as s3.CfnBucket; // Change its properties cfnBucket.analyticsConfiguration = [ { id: 'Config', // ... } ];
JavaScript
// Get the CloudFormation resource const cfnBucket = bucket.node.defaultChild; // Change its properties cfnBucket.analyticsConfiguration = [ { id: 'Config' // ... } ];
Python
# Get the CloudFormation resource cfn_bucket = bucket.node.default_child # Change its properties cfn_bucket.analytics_configuration = [ { "id": "Config", # ... } ]
Java
// Get the CloudFormation resource CfnBucket cfnBucket = (CfnBucket)bucket.getNode().getDefaultChild(); cfnBucket.setAnalyticsConfigurations( Arrays.asList(java.util.Map.of( // Java 9 or later "Id", "Config", // ... ));
C#
// Get the CloudFormation resource var cfnBucket = (CfnBucket)bucket.Node.DefaultChild; cfnBucket.AnalyticsConfigurations = new List<object> { new Dictionary<string, string> { ["Id"] = "Config", // ... } };

You can also use this object to change AWS CloudFormation options such as Metadata and UpdatePolicy.

TypeScript
cfnBucket.cfnOptions.metadata = { MetadataKey: 'MetadataValue' };
JavaScript
cfnBucket.cfnOptions.metadata = { MetadataKey: 'MetadataValue' };
Python
cfn_bucket.cfn_options.metadata = { "MetadataKey": "MetadataValue" }
Java
cfnBucket.getCfnOptions().setMetadata(java.util.Map.of( // Java 9+ "MetadataKey", "Metadatavalue"));
C#
cfnBucket.CfnOptions.Metadata = new Dictionary<string, object> { ["MetadataKey"] = "Metadatavalue" };

Use un-escape hatches

The AWS CDK also provides the capability to go up an abstraction level, which we might refer to as an "un-escape" hatch. If you have an L1 construct, such as CfnBucket, you can create a new L2 construct (Bucket in this case) to wrap the L1 construct.

This is convenient when you create an L1 resource but want to use it with a construct that requires an L2 resource. It's also helpful when you want to use convenience methods like .grantXxxxx() that aren't available on the L1 construct.

You move to the higher abstraction level using a static method on the L2 class called .fromCfnXxxxx()—for example, Bucket.fromCfnBucket() for Amazon S3 buckets. The L1 resource is the only parameter.

TypeScript
b1 = new s3.CfnBucket(this, "buck09", { ... }); b2 = s3.Bucket.fromCfnBucket(b1);
JavaScript
b1 = new s3.CfnBucket(this, "buck09", { ...} ); b2 = s3.Bucket.fromCfnBucket(b1);
Python
b1 = s3.CfnBucket(self, "buck09", ...) b2 = s3.from_cfn_bucket(b1)
Java
CfnBucket b1 = CfnBucket.Builder.create(this, "buck09") // .... .build(); IBucket b2 = Bucket.fromCfnBucket(b1);
C#
var b1 = new CfnBucket(this, "buck09", new CfnBucketProps { ... }); var v2 = Bucket.FromCfnBucket(b1);

L2 constructs created from L1 constructs are proxy objects that refer to the L1 resource, similar to those created from resource names, ARNs, or lookups. Modifications to these constructs do not affect the final synthesized AWS CloudFormation template (since you have the L1 resource, however, you can modify that instead). For more information on proxy objects, see Referencing resources in your AWS account.

To avoid confusion, do not create multiple L2 constructs that refer to the same L1 construct. For example, if you extract the CfnBucket from a Bucket using the technique in the previous section, you shouldn't create a second Bucket instance by calling Bucket.fromCfnBucket() with that CfnBucket. It actually works as you'd expect (only one AWS::S3::Bucket is synthesized) but it makes your code more difficult to maintain.

Use raw overrides

If there are properties that are missing from the L1 construct, you can bypass all typing using raw overrides. This also makes it possible to delete synthesized properties.

Use one of the addOverride methods (Python: add_override) methods, as shown in the following example.

TypeScript
// Get the CloudFormation resource const cfnBucket = bucket.node.defaultChild as s3.CfnBucket; // Use dot notation to address inside the resource template fragment cfnBucket.addOverride('Properties.VersioningConfiguration.Status', 'NewStatus'); cfnBucket.addDeletionOverride('Properties.VersioningConfiguration.Status'); // use index (0 here) to address an element of a list cfnBucket.addOverride('Properties.Tags.0.Value', 'NewValue'); cfnBucket.addDeletionOverride('Properties.Tags.0'); // addPropertyOverride is a convenience function for paths starting with "Properties." cfnBucket.addPropertyOverride('VersioningConfiguration.Status', 'NewStatus'); cfnBucket.addPropertyDeletionOverride('VersioningConfiguration.Status'); cfnBucket.addPropertyOverride('Tags.0.Value', 'NewValue'); cfnBucket.addPropertyDeletionOverride('Tags.0');
JavaScript
// Get the CloudFormation resource const cfnBucket = bucket.node.defaultChild ; // Use dot notation to address inside the resource template fragment cfnBucket.addOverride('Properties.VersioningConfiguration.Status', 'NewStatus'); cfnBucket.addDeletionOverride('Properties.VersioningConfiguration.Status'); // use index (0 here) to address an element of a list cfnBucket.addOverride('Properties.Tags.0.Value', 'NewValue'); cfnBucket.addDeletionOverride('Properties.Tags.0'); // addPropertyOverride is a convenience function for paths starting with "Properties." cfnBucket.addPropertyOverride('VersioningConfiguration.Status', 'NewStatus'); cfnBucket.addPropertyDeletionOverride('VersioningConfiguration.Status'); cfnBucket.addPropertyOverride('Tags.0.Value', 'NewValue'); cfnBucket.addPropertyDeletionOverride('Tags.0');
Python
# Get the CloudFormation resource cfn_bucket = bucket.node.default_child # Use dot notation to address inside the resource template fragment cfn_bucket.add_override("Properties.VersioningConfiguration.Status", "NewStatus") cfn_bucket.add_deletion_override("Properties.VersioningConfiguration.Status") # use index (0 here) to address an element of a list cfn_bucket.add_override("Properties.Tags.0.Value", "NewValue") cfn_bucket.add_deletion_override("Properties.Tags.0") # addPropertyOverride is a convenience function for paths starting with "Properties." cfn_bucket.add_property_override("VersioningConfiguration.Status", "NewStatus") cfn_bucket.add_property_deletion_override("VersioningConfiguration.Status") cfn_bucket.add_property_override("Tags.0.Value", "NewValue") cfn_bucket.add_property_deletion_override("Tags.0")
Java
// Get the CloudFormation resource CfnBucket cfnBucket = (CfnBucket)bucket.getNode().getDefaultChild(); // Use dot notation to address inside the resource template fragment cfnBucket.addOverride("Properties.VersioningConfiguration.Status", "NewStatus"); cfnBucket.addDeletionOverride("Properties.VersioningConfiguration.Status"); // use index (0 here) to address an element of a list cfnBucket.addOverride("Properties.Tags.0.Value", "NewValue"); cfnBucket.addDeletionOverride("Properties.Tags.0"); // addPropertyOverride is a convenience function for paths starting with "Properties." cfnBucket.addPropertyOverride("VersioningConfiguration.Status", "NewStatus"); cfnBucket.addPropertyDeletionOverride("VersioningConfiguration.Status"); cfnBucket.addPropertyOverride("Tags.0.Value", "NewValue"); cfnBucket.addPropertyDeletionOverride("Tags.0");
C#
// Get the CloudFormation resource var cfnBucket = (CfnBucket)bucket.node.defaultChild; // Use dot notation to address inside the resource template fragment cfnBucket.AddOverride("Properties.VersioningConfiguration.Status", "NewStatus"); cfnBucket.AddDeletionOverride("Properties.VersioningConfiguration.Status"); // use index (0 here) to address an element of a list cfnBucket.AddOverride("Properties.Tags.0.Value", "NewValue"); cfnBucket.AddDeletionOverride("Properties.Tags.0"); // addPropertyOverride is a convenience function for paths starting with "Properties." cfnBucket.AddPropertyOverride("VersioningConfiguration.Status", "NewStatus"); cfnBucket.AddPropertyDeletionOverride("VersioningConfiguration.Status"); cfnBucket.AddPropertyOverride("Tags.0.Value", "NewValue"); cfnBucket.AddPropertyDeletionOverride("Tags.0");

Use custom resources

If the feature isn't available through AWS CloudFormation, but only through a direct API call, you must write an AWS CloudFormation Custom Resource to make the API call you need. You can use the AWS CDK to write custom resources and wrap them into a regular construct interface. From the perspective of a consumer of your construct, the experience will feel native.

Building a custom resource involves writing a Lambda function that responds to a resource's CREATE, UPDATE, and DELETE lifecycle events. If your custom resource needs to make only a single API call, consider using the AwsCustomResource. This makes it possible to perform arbitrary SDK calls during an AWS CloudFormation deployment. Otherwise, you should write your own Lambda function to perform the work you need to get done.

The subject is too broad to cover completely here, but the following links should get you started: