AWS CDK vs CloudFormation - Comparison


Borislav Hadzhiev

Wed Apr 13 20226 min read


Photo by Denny Ryanto

Updated - Wed Apr 13 2022

Comparing AWS CDK vs CloudFormation #

We are going to compare AWS CDK and CloudFormation as ways to provision our infrastructure in the AWS ecosystem.

Maintainability #

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.

In general, in programming, the less code we have to maintain - the better. If your code works and is readable, then the less you have - the better.

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: '', }); } } // 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'}, });
Note: by default the 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.

vpc construct

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.

Good Developer Experience #

Our infrastructure solution should be intuitive to use and provide sane defaults.

CDK provides 3 different levels of constructs:

  1. Cfn Resources - 1x1 mapping to the corresponding CloudFormation Resource
  2. Level 2 Constructs - opinionated components that provide sane defaults and a higher level of abstraction
  3. Level 3 Constructs (the highest level of abstraction) - complete patterns, i.e. an API with a Lambda integration with an Aurora Serverless database

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).

If we really wanted to write CloudFormation, we could write CloudFormation in TypeScript/Python etc, using CDK with Cfn Resources, however, there's no good reason to do something like that.

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.

Ability to use Abstractions written by other Developers #

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.

A declarative way to express interactions between resources #

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.

s3 grant perms

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.

Ability to use Programming Logic, Loops #

When you write your infrastructure using CDK - you're using a programming language, so you can use conditional logic and loops.

You can extract functions and classes that provide commonly used functionality and reuse them all throughout your code base.

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.

Ability to test our code #

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.

Summary #

I can't think of a good reason one would use CloudFormation over CDK.

CDK provides all the Cfn Resources plus constructs that provide higher levels of abstraction and save us a lot of time and lines of code to write and maintain.

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.

Further Reading #

Use the search field on my Home Page to filter through my more than 1,000 articles.