Last updated: Jan 27, 2024
Reading timeยท9 min
Note: If you want to read about Escape Hatches in CDK, click on the last subheading
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.
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.
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.:
Let's start with an example of a CfnBucket.
Like every other type of construct, Cfn Resources take in 3 parameters:
this
keyword, in Python -
self
, etc.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.
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.
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:
In summary, I only use CloudFormation documentation to understand the behavior of configuration properties.
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:
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.
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.
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.
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.
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.
In order to use CfnResources in AWS CDK we have to refer to 2 types of documentation - the CDK and CloudFormation docs.
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:
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:
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.
We access the node.defaultChild
property on the Level 2 User Pool
construct and cast it as the CfnUserPool
resource.
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:
Define the Dynamodb table using the Level 2 Table construct.
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
Now that we have access to the CloudFormation resource we are able to set any properties and access any methods, made available in CloudFormation
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.
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.
Cfn constructs are named in the form of Cfn
+ the name of the resource, for
example:
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.
You can learn more about the related topics by checking out the following tutorials: