Using Cfn Resources and Escape Hatches in AWS CDK

avatar
Borislav Hadzhiev

Last updated: Jan 27, 2024
9 min

banner

# Table of Contents

  1. What are Cfn Resources
  2. Using Cfn Resources
  3. Using Escape Hatches in AWS CDK

Note: If you want to read about Escape Hatches in CDK, click on the last subheading

# What are Cfn Resources

Cfn Resources are a type of Construct in AWS CDK.

You can think of Constructs as Cloud components. We use them to encapsulate logic, which we can reuse throughout our infrastructure code.

There are 3 levels of Constructs in the AWS CDK constructs library and Cfn Resources are Level 1 constructs.

They are 1x1 mappings to the resource in CloudFormation, which means they provide the lowest level of abstraction on top of CloudFormation.

Since our CDK code compiles down to CloudFormation, we generally look for higher level of constructs that would enable us to hide some of the complexity that comes along with using CloudFormation.

However, we often have to use Cfn Resources, just because the CDK constructs library generates and exposes them automatically, immediately after a new resource is introduced in CloudFormation.

When we use Cfn Resources, we're basically writing CloudFormation using a programming language, instead of a configuration language (YAML or JSON).

They don't provide any of the opinionated defaults implemented in level 2 and level 3 constructs, nor do they provide any of the glue logic for service to service interactions, i.e. permission grants.

# Using Cfn Resources

When we need to use Cfn Resources, we often have to refer to the CloudFormation docs.

The naming convention for Cfn Resources is Cfn + the resource's name, i.e.:

  • CfnBucket
  • CfnBucketPolicy
  • CfnFunction
  • CfnTable

Let's start with an example of a CfnBucket.

Like every other type of construct, Cfn Resources take in 3 parameters:

  • the scope - specifies the parent construct within which the child construct is initialized, i.e. in JavaScript - the this keyword, in Python - self, etc.
  • the identifier - must be unique within the scope. The combination of CDK identifiers builds the CloudFormation logical ID of the resource.
  • props - A map of key-value pairs where we pass in configuration options. This parameter is optional.

Before we start with the examples, it's very important to note that when using Cfn Resources, we are really just writing CloudFormation using a programming language.

If we look at the properties sections of the CfnBucket construct and the AWS::S3::Bucket CloudFormation resource we can see that the only difference is the casing.

CloudFormation properties use PascalCase, whereas CDK configuration properties use camelCase (in TypeScript).

All of the properties in the CDK and CloudFormation documentation are the same, however, the CloudFormation documentation offers us way more information about what the properties actually do.

Let's look at an example of a CfnBucket first:

import * as cdk from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; export class MyCdkStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐Ÿ‘‡ using the CfnBucket construct const bucket = new s3.CfnBucket(this, 'uploads-bucket', { corsConfiguration: { corsRules: [ { allowedMethods: [ s3.HttpMethods.GET, s3.HttpMethods.POST, s3.HttpMethods.PUT, ], allowedOrigins: ['http://localhost:3000'], allowedHeaders: ['*'], }, ], }, tags: [{key: 'environment', value: 'development'}], }); bucket.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); } }

In the snippet above, we use the CfnBucket construct to create an S3 bucket, passing in CORS configuration options and a tag.

We then call the applyRemovalPolicy method on the CfnBucket to set the deletion policy of the bucket to DESTROY.

The hard part is finding out what properties we need to use and the types and structure of said properties.

# How to read docs for CFN Resources

It can be quite confusing to understand where to look for the information we need because most of the time we have to refer to documentation in 2 places:

A formula I use is:

  • Refer to CloudFormation docs for properties, whose behavior I don't understand. In general the CDK docs only specify types for construct properties of Cfn Resources, whereas the CloudFormation docs explain the behavior of properties.
  • After I know what the property does, I use the CDK docs to understand the structure and type of the property
  • Refer to the CDK docs for helper methods
  • Try to use my IDE's auto-completion as much as possible. If you use a modern IDE with a typed language you should be able to stay in the IDE most of the time.

In summary, I only use CloudFormation documentation to understand the behavior of configuration properties.

# Another example of using Cfn Resources

Another example of using CfnResources that you'll use in almost every CDK application is the CfnOutput construct.

Outputs in CloudFormation and CDK enable us to output values from a stack, so:

  • we can reference them from another stack
  • we can redirect the output values to a file and import the values from our frontend code. A good example is outputting the value of an API Gateway URL so our frontend can use it.

In this example, we are going to output the name of the S3 bucket we created using the CfnOutput construct.

const bucketNameOutput = new cdk.CfnOutput(this, 'bucketName', { value: bucket.bucketName || '', description: 'The name of the s3 bucket for avatar uploads', exportName: 'avatarsBucket', }); bucketNameOutput.overrideLogicalId('MyBucket');

In the example above we define our CfnOutput resource passing it the name of the S3 bucket as a value.

We then use the overrideLogicalId method on the Cfn Resource to override the CloudFormation logical ID of the Output.

The equivalent resource in CloudFormation would look as follows.

template.yaml
Outputs: MyBucket: Value: !Ref bucket Description: 'The name of the s3 bucket for avatar uploads' Export: Name: avatarsBucket

Let's take a look at the documentation of outputs in both CDK and CloudFormation:

In this case, the properties in the CDK docs have a short explanation that has been copied from the CloudFormation docs.

Ideally, in the future, we'll be able to get all the information we need for CfnResources just by using the CDK documentation because there are some small inconsistencies between the names of some properties.

# Defining Cfn Resources manually

In some very rare cases, we have to manually define a Cfn Resource using the CfnResource class.

If, for example, the CfnTable resource wasn't available, we would have to use the CfnResource class and define a table ourselves.

It is very unlikely that you'll have to do this often, however, it's good to know how to do it, just in case.

Let's create a Dynamodb table using the CfnResource class.

What we'll notice is that we're basically using CloudFormation, the property casing as well as the structure of properties is the same as in CloudFormation.

Here are the CloudFormation Dynamodb table docs.

new cdk.CfnResource(this, 'MyTable', { type: 'AWS::DynamoDB::Table', properties: { KeySchema: [ { AttributeName: 'todoId', KeyType: 'HASH', }, { AttributeName: 'date', KeyType: 'RANGE', }, ], AttributeDefinitions: [ {AttributeName: 'todoId', AttributeType: 'S'}, {AttributeName: 'date', AttributeType: 'S'}, ], BillingMode: 'PAY_PER_REQUEST', }, });

If you open the CloudFormation docs for the Dynamodb tables, you'll see that the configuration properties are the exact same.

As a comparison, the CloudFormation template for the DynamoDB table would as follows.

template.yaml
Resources: MyTable: Type: AWS::DynamoDB::Table Properties: KeySchema: - AttributeName: todoId KeyType: HASH - AttributeName: date KeyType: RANGE AttributeDefinitions: - AttributeName: todoId AttributeType: S - AttributeName: date AttributeType: S BillingMode: PAY_PER_REQUEST

We should avoid using the CfnResource class when we can because we get no autocompletion in our IDE.

If we were defining a more complicated resource it would be a very error-prone process.

Even when using the Level 1 Cfn constructs, provided by the CDK team, there's a lot of work that's already been done for us behind the scenes.

# Conclusion

In order to use CfnResources in AWS CDK we have to refer to 2 types of documentation - the CDK and CloudFormation docs.

It's best to avoid using CfnResources if we can, however, it's not always an option because Cfn Resources get generated automatically and are exposed almost immediately after release.

It takes time for the AWS CDK team to provide a higher level of abstraction for some resources in the form of Level 2 or Level 3 constructs.

For now, the best way to work with Cfn Resources is to:

  • Read about the properties of the resources in the CloudFormation documentation.
  • Get everything else from the CDK docs - i.e. types/structure of properties, available methods on the resources, etc.

# Using Escape Hatches in AWS CDK

If we need to customize a resource property that is not exposed by the higher level Construct we're using, then we need to use an escape hatch.

In order to use an escape hatch in CDK, we have to get access to the Level 1 Cfn Resource. We can do that by accessing the node.defaultChild property on the construct.

A common case where we have to use an escape hatch because of functionality that's not yet implemented in the higher level construct is when modifying the email configuration of a Cognito user pool:

// ๐Ÿ‘‡ define user pool using Level 2 construct const userPool = new cognito.UserPool(this, 'userpool', { // ...props }); // ๐Ÿ‘‡ access the node.defaultChild property (escape hatch) const cfnUserPool = userPool.node.defaultChild as cognito.CfnUserPool; // ๐Ÿ‘‡ work with the CloudFormation resource cfnUserPool.emailConfiguration = { emailSendingAccount: 'DEVELOPER', replyToEmailAddress: 'john@example.com', sourceArn: `arn:aws:ses:us-east-1:123456789:identity/john@example.com`, };

In the code snippet:

  1. We define our User Pool using a Level 2 construct. Unfortunately, at the time of writing the Level 2 construct does not support updating the email configuration, so we have to use an escape hatch.

  2. We access the node.defaultChild property on the Level 2 User Pool construct and cast it as the CfnUserPool resource.

  3. We can now set the emailConfiguration property on the Level 1 CfnUserPool construct.

Let's look at another example, this time we'll use a Dynamodb table:

// ๐Ÿ‘‡ table definition using Level 2 construct const table = new dynamodb.Table(this, 'my-table', { partitionKey: {name: 'todoId', type: dynamodb.AttributeType.NUMBER}, }); // ๐Ÿ‘‡ get access to the Level 1 Cfn resource (escape hatch) const cfnTable = table.node.defaultChild as dynamodb.CfnTable; // ๐Ÿ‘‡ update properties on the Level 1 Cfn resource cfnTable.billingMode = dynamodb.BillingMode.PROVISIONED; cfnTable.provisionedThroughput = { readCapacityUnits: 1, writeCapacityUnits: 1, }; cfnTable.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); cfnTable.tags.setTag('env', 'dev');

In our code snippet we:

  1. Define the Dynamodb table using the Level 2 Table construct.

  2. We get the Level 1 Cfn resource by accessing the node.defaultChild property on the Level 2 construct. Note that we also cast the Cfn resource as the appropriate type, so we can then access any properties and methods available on the CfnTable construct

  3. Now that we have access to the CloudFormation resource we are able to set any properties and access any methods, made available in CloudFormation

# Escape Hatches - Discussion

When working with constructs in CDK we aim to use higher levels of abstraction.

Higher levels of abstraction allow us to take advantage of some of the sane defaults and glue methods for service-to-service interactions put in place by the CDK team in the CDK constructs library.

On the other hand, it's inevitable that by using higher levels of abstraction we lose some of the ability to customize certain resources.

If we need to customize a resource property that is not exposed by the Construct we're using, then we need to use an escape hatch.

We only use escape hatches where we need to fill a gap and interact with configuration properties that are not accessible on a higher-level construct.

Our CDK code eventually gets compiled into CloudFormation, which is very customizable, however, it also is quite verbose and difficult to manage at scale.

Escape hatches give us the ability to use the best of both worlds.

We can write our code using higher levels of abstraction with Level 2 and Level 3 constructs, however, we are also able to use an escape hatch and access the CloudFormation (Level 1) resource and update its properties.

Higher-level constructs are just wrappers around the Level 1 Cfn constructs, which means that in order to use an escape hatch we have to find a way to access the Cfn construct.

A Cfn construct is a 1x1 mapping to the corresponding CloudFormation resource and enables us to use all of the configuration properties available in CloudFormation.

Cfn constructs are named in the form of Cfn + the name of the resource, for example:

  • CfnBucket
  • CfnTable
  • CfnFunction

We can access the Cfn resource of a higher-level construct by using construct.node.defaultChild.

After we get access to a Cfn resource we have to cast its type and then we are able to set and access any of the properties and methods made available in CloudFormation.

The ability to use escape hatches enables us to write our code using higher level of abstractions and only revert back to CloudFormation resources in the case a functionality is not exposed to the user of the construct.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev