Borislav Hadzhiev
Wed Apr 13 2022·6 min read
Photo by Denny Ryanto
Updated - Wed Apr 13 2022
We are going to compare AWS CDK and CloudFormation as ways to provision our infrastructure in the AWS ecosystem.
A good solution for provisioning infrastructure should be easy to maintain, update and extend. When new engineers join the team, they should be able to get up to speed pretty quickly after reading our infrastructure code.
Let's compare provisioning a VPC in CDK and CloudFormation. The necessary code to provision a VPC in CDK is:
import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; // define the CDK Stack export class MyVpcStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // use the ec2.Vpc construct const myVpc = new ec2.Vpc(this, 'my-vpc', { cidr: '10.0.0.0/16', }); } } // Instantiate the CDK App const app = new cdk.App(); // Instantiate the CDK Stack new MyVpcStack(app, `cdk-constructs-stack-dev`, { stackName: `cdk-constructs-stack-dev`, env: {region: process.env.CDK_DEFAULT_REGION}, tags: {env: 'dev'}, });
Vpc
construct provisions one NAT gateway per Availability Zone. NAT Gateways are priced at an hourly rate.With the 3 lines of instantiating the Vpc
construct we provisioned 24
CloudFormation resources in our stack.
For a comparison, the equivalent CloudFormation template is 282 lines long.
It's not just the difference in lines of code - maintaining 282 lines of relationships between all of the resources that make up a VPC adds a lot of complexity to our infrastructure.
CDK is the clear winner when it comes to maintainability of infrastructure code.
Our infrastructure solution should be intuitive to use and provide sane defaults.
CDK provides 3 different levels of constructs:
Whereas Cloudformation only provides the vanilla Cfn Resources - these are low level and unopinionated. They don't provide any glue logic for service to service interactions, so we end up having to be quite explicit (verbose) in our infrastructure provisioning approach.
As a Developer I'd rather focus on updating the configuration properties of resources when my use case does not match the default behavior, rather than having to explicitly set every configuration property because the solution is unopinionated (CloudFormation).
The different abstraction levels CDK provides enable us to focus on the bigger picture until we have to change the default behavior, which is the more intuitive approach and provides better developer experience.
Another big win for CDK on the developer experience front is not having to context switch between a programming language (typescript, python) and a configuration language (yaml, json).
I am not a big fan of the IDE support that comes with writing YAML or JSON, compared to TypeScript. Maintaining large YAML files always slows teams down. YAML was never intended to be used in a way where the configuration files can easily grow to thousands of lines of code.
I'll take the code completion, IDE support and linting of TypeScript compared to YAML.
We should let the computers compile down our CDK code into CloudFormation. Humans can more easily understand and manage CDK code.
As a developer I'd rather not have to write every bit of code myself. Especially on the backend where I'm not worried about bundle size, I'd rather just use an npm package that solves my problem and abstracts some of the complexity away.
The AWS team maintains an official collection of packages written in 4 languages - Python, Java, .NET, TypeScript, which we can use in our CDK applications.
These packages provide a well documented way to define AWS resources that come with sane defaults and glue code for service to service interactions. This is code we don't have to write or manage - if there is a solution for the infrastructure we are currently facing - we can just plug in an official package written by the AWS team.
As a comparison, the closest thing to reusing code CloudFormation thing another developer has written is copy and pasting their Code into your yaml template.
Having a more declarative and direct approach reduces complexity in code.
Don't get me wrong, having a good understanding of how the different AWS services play together is great, but I'd rather be able to use utility functions to express my intent in a more direct way.
For instance with CDK we are able to use predefined utility methods for commonly required functionality. I.e. this is how an S3 bucket grants permissions to a Lambda function using CDK:
export class MyStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // instantiate the s3.Bucket construct const bucket = new s3.Bucket(this, 'my-bucket'); // instantiate the lambda.Function construct const lambdaFunction = new lambda.Function(this, 'my-function', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'main', code: lambda.Code.fromAsset( path.join(__dirname, '../src/my-function-path'), ), }); // use utility methods to grant permissions to the Lambda bucket.grantPut(lambdaFunction); bucket.grantPutAcl(lambdaFunction); } }
With just two lines of code, we were able to provision a policy that follows the
best practice of least privilege and grants our lambda permissions for the
s3:PutObject
and s3:PutObjectAcl
actions.
With CloudFormation you would have to explicitly define the policy and role and associate them with the Lambda function.
Having to maintain less code because of the utilities CDK provides and my code being more declarative and readable is a win-win.
When you write your infrastructure using CDK - you're using a programming language, so you can use conditional logic and loops.
With CloudFormation we have access to some condition functions, but that's about it. They are difficult to use and come with many constraints. There are multiple ways to do the same thing, using different syntax, which is just confusing.
As you could imagine testing YAML or JSON code is much harder than testing TypeScript or Python code.
For example in TypeScript we can use the jest
testing framework to test our
CDK code.
When we write CloudFormation we have to deploy our stack and wait for the result, which is not great.
Ideally we'd rather fail fast. With CDK we can write tests that check if a resource has a specific property set to a specific value. We can validate resource configuration and choose to exit early and throw an error when our validation fails.
If the consumers of our CDK constructs pass in invalid data, we are able to throw an error and inform them, rather than deploy and find out.
I can't think of a good reason one would use CloudFormation over CDK.
Having to context switch between a programming language (typescript, python) and a configuration language (yaml, json) adds to the complexity of managing our infrastructure.
The IDE integration and support we get from using programming languages such as TypeScript is way better than any YAML plugin or extension ever created.
A couple of years ago I had to write/manage CloudFormation and AWS SAM templates that were 1,000 - 5,000 lines of code - at the time these were the only solutions you had to pick from.
It was a nightmare to go back to a project after a few weeks/months, every time I'd have to spend a day reading through the yaml template, all the provisioned resources and all the interactions.
Right now there is no reason to use these services for any of the new projects you start, CDK is the clear winner.