The AWS Cloud Development Kit (CDK) allows you to define your AWS resources using the programming languages you know and love. This concept piqued the interest of many of us here at Instil; when someone offers us the ability to use Typescript instead of YAML we’re sold!
I have been using CDK for the past 3 years for container based and serverless projects, and what I think is CDK’s greatest strength are the guard rails it provides to the developer:
- Like any good API it is self-documenting, the declarative style helps to guide you on what resources are compatible with each other without the need to know about every feature in every AWS service.
- The built-in helpers for generating IAM roles give you safe defaults to ensure you are following security best practices.
- The high level (level 3) constructs as well as open source versions (like CDK patterns and Construct Hub) accelerate your application development by providing patterns that ensure your architecture is scalable, cost effective and secure.
Despite all of CDK’s strengths it’s important to know the weaknesses of the framework and protect your team from any pitfalls they could encounter. Here are our top 5 lessons we have learnt on our CDK journey.
1. Separate your stateless and stateful resources into different stacks
Keeping your stateful resources, (e.g. DynamoDB tables, RDS instances or Cognito User Pools) in separate stacks to your stateless resources, (e.g. Lambda functions or ECS Services) means that you will be able to delete and re-create your stateless stacks without losing any data during the process. This is a lesson we learnt early on our CDK journey and has proven a useful pattern across multiple projects.
2. Turn on termination protection for your production environments
It’s very easy for a developer to log in to the wrong AWS account and accidentally delete a CloudFormation stack (don’t ask me how I know this!). Turning on termination protection is an extra layer of protection that helps avoid those pesky developers taking down an environment by mistake 🙈. Using CDK makes it easy to include conditional logic to only enable termination protection in production accounts, reducing overhead on developers day to day.
https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.Stack.html#terminationprotection
Note: I used the term "extra layer" of protection. Termination protection shouldn't be used as an excuse for poorly managed IAM roles with more privileges than required!
3. Refactoring isn't as easy as you think
This is a lesson that has taken us longer to learn. We love clean code at Instil, refactoring is everything to us! But because CDK is “code” you can be lulled into a false sense of security when it comes to refactoring. Simply moving a resource from one stack to another could cause it to be deleted and recreated. Sometimes you can have a beautifully refactored stack, only to learn at deployment time that you have broken something as a resource is referenced in other stacks.
A rule of thumb we like to follow is this: when a stack has been deployed to production, take extra care when refactoring. Deploy the original version to your own developer account first, and then try to deploy your refactored version.
Be careful not to change the logical ID of your CDK resources, this will result in a deletion and re-creation of the resource.
4. Use the IAM convenience methods
One of the best guard rails provided by CDK are the convenience methods for automatically creating IAM roles. For example if you had a lambda function that you wanted to have read-only access to a single dynamodb table:
myDynamoTable.grantReadData(myLambdaFunction);
Most of these convenience methods start with .grant
making discoverability easy in your IDE.
Using these convenience methods makes it easier to follow the principle of least privilege without having to write your own IAM roles. If you find yourself writing your own roles, think twice and investigate if a convenience method is there instead.
5. Use abstractions carefully
There are multiple levels of constructs that CDK provides:
- L1 - “L1 constructs are exactly the resources defined by AWS CloudFormation”
- L2 - “L2 constructs also represent AWS resources, but with a higher-level, intent-based API. They provide similar functionality, but provide the defaults, boilerplate, and glue logic you'd be writing yourself with a CFN Resource construct”
- L3 - “These constructs are designed to help you complete common tasks in AWS, often involving multiple kinds of resources.” These are more like an opinionated pattern.
It is tempting to build a bunch of “L2.5” constructs that are specific to your project. For example ProjectNameS3Bucket. There is a time and a place where you might want to use these in a project, especially when you want to avoid duplicating code. However we recommend that you don't try to do this too early in the project. Try to keep these pointers in mind:
- Are you reducing flexibility of the underlying L2 construct? - It’s hard to make one configuration of a bucket useful across multiple stacks in a project.
- Do your default values work for all use cases of your construct? - Could someone change a default property of the shared construct that unknowingly breaks another part of the application?
I hope that you found this helpful. As I said at the beginning there are many strengths with CDK but it is important to be aware of the pitfalls. If you or your team want to learn more about CDK, be sure to check out our TypeScript for AWS Serverless Course.