Building Applications with AWS CDK
In a previous post I explained how to build a serverless application using Serverless Framework. In this post, I’ll explain how to bring the same application to life using AWS CDK and I’ll compare the two approaches.
Architecture
The application is the same as before: every hour headlines are pulled from an rss feed and peristed to S3. This time it will use AWS CDK and the source for this version is here.

Installation
You can run cdk via npx but I’ll be referring to it often so I install it globally:
bashnpm install -g aws-cdk
To deploy lambda code, the AWS account and region we are deploying to needs to be Bootstrapped. Doing so will create a CloudFormation stack called CDKToolkit that CDK will use when deploying our application stacks. Running the following command will bootstrap your environment:
bashcdk bootstrap aws://<aws-account>/<region>
If you already have credentials cached in your aws-cli, you run the following to get your aws account for the above command:
bashaws sts get-caller-identity
Initialisation
Initialise the project with the following:
bashmkdir headlines-cdk cd headlines-cdk cdk init --language typescript
Within the result we can see the definition of our new (empty) stack in the file lib/headlines-cdk-stack.ts:
typescriptexport class HeadlinesCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); } }
We’ll use the above file to define the infrastructure in our stack and CDK will compile this to create a CloudFormation template. We can see the output of the CloudFormation template by running:
bashcdk synthesize
And we can deploy the stack with:
bashcdk deploy
And you can bring the whole thing back down again with:
bashcdk destroy
Adding Resources
Adding an S3 Bucket is as simple as adding the following to our HeadlinesCdkStack class:
typescriptconst bucket = new s3.Bucket(this, "headlines-cdk-bucket");
We can run a diff to see the difference between our currently defined stack and what’s currently deployed:
bash$ cdk diff Stack HeadlinesCdkStack Resources [+] AWS::S3::Bucket headlines-cdk-bucket headlinescdkbucket80B561CD
I’ve added the same parseFeed.ts function implementation to src for our lambda. While the lambda.function construct supports multiple languages via its runtime property, the lambda-nodejs.NodejsFunction construct will take care of converting our typescript function implementation into javascript. The last line below will generate the permissions needed for the lambda to write to the bucket.
typescriptconst handler = new NodejsFunction(this, "headlines-cdk-parseFeed", { handler: "handle", entry: path.join(__dirname, `/../src/parseFeed.ts`), environment: { BUCKET_NAME: bucket.bucketName, }, }); bucket.grantReadWrite(handler);
Finally, the lambda should be triggered once an hour. To do this we’ll create a Rule with a one hour Schedule and attach the lambda as its target.
typescriptconst eventTarget = new targets.LambdaFunction(handler); new Rule(this, "headless-cdk-schedule", { schedule: Schedule.rate(cdk.Duration.hours(1)), targets: [eventTarget], });
The entire infrastructure definition, ready to be cdk deployed, looks like this:
typescriptimport * as cdk from "@aws-cdk/core"; import * as s3 from "@aws-cdk/aws-s3"; import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; import * as path from "path"; import { Rule, Schedule } from "@aws-cdk/aws-events"; import * as targets from "@aws-cdk/aws-events-targets"; export class HeadlinesCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const bucket = new s3.Bucket(this, "headlines-cdk-bucket"); const handler = new NodejsFunction(this, "headlines-cdk-parseFeed", { handler: "handle", entry: path.join(__dirname, `/../src/parseFeed.ts`), environment: { BUCKET_NAME: bucket.bucketName, }, }); bucket.grantReadWrite(handler); const eventTarget = new targets.LambdaFunction(handler); new Rule(this, "headless-cdk-schedule", { schedule: Schedule.rate(cdk.Duration.hours(1)), targets: [eventTarget], }); } }
Comparing AWS CDK to Serverless Framework
There are pros and cons with each approach and in the follow sections I’ll compare the two and use ✅ to denote which framework performs better.
Definition
With Serverless Framework we can use serverless.ts to gain type safety but our config is ~60 lines. Switching YAML (below) drops this to about ~40. On the CDK side of things, the definition is ~10 lines plus imports.
Whilst Serverless Framework can raise infrastructure based on function and events configuration, items like ElasticSearch, SQS, S3 must be defined under Resources and are CloudFormation syntax. CDK on the other-hand abstracts away so much CloudFormation that you might not write any. Syntax in CDK like bucket.grantReadWrite(handler) are a great way for developers to onboard to IAM policies without even knowing policy syntax.
Variables and references are much nicer in CDK due to them being plain TypeScript objects, whereas in Serverless Framework usage of CloudFormation means references are accompanied by the unappealing syntax of Intrinsic Functions.
CDK ✅
yaml# severless.yml example --- service: headlines frameworkVersion: "2" configValidationMode: error custom: webpack: webpackConfig: "./webpack.config.js" includeModules: true excludeFiles: src/**/*.test.ts bucketName: headlines-${sls:stage} plugins: - serverless-webpack - serverless-offline provider: name: aws region: us-west-2 runtime: nodejs12.x versionFunctions: false iam: role: statements: - Effect: Allow Action: - s3:PutObject Resource: arn:aws:s3:::${self:custom.bucketName}/* functions: parseFeed: handler: src/parseFeed.handle events: - schedule: rate: rate(1 hour) environment: BUCKET_NAME: "${self:custom.bucketName}" resources: Resources: Bucket: Type: AWS::S3::Bucket Properties: BucketName: "${self:custom.bucketName}"
Imports
Every construct we specify in CDK is shipped in a different package. This means we’ll need to first identify which packages we need, npm install each, and import each. Any update to one package, is required to tall packages. In CDK 2, which is currently in Developer Preview, reduces this to a single package:
AWS CDK v2 consolidates the AWS Construct Library into a single package; developers no longer need to install one or more individual packages for each AWS service.
Serverless Framework ✅
Removing a stack
When removing a stack, empty buckets will not be deleted. cdk destroy will entirely remove the stack but leave a non-empty bucket present without warning. sls remove will attempt and fail to delete the bucket, leaving the stack a DELETE_FAILED state, allowing the user to manually delete the stack and to choose whether they want the bucket removed.
Validation
AWS resources have certain rules that a developer must be aware of, for example, that lambdas can’t be scheduled at intervals less than 1 minute.
AWS Lambda supports standard rate and cron expressions for frequencies of up to once per minute.
When setting a value less than a minute, say, rate(10 seconds), both frameworks will throw an error at compile time. Neither framework identifies where in our code the break has ocurred, and the messages hint at the reason but still require some interpretation.
bash# cdk error /Users/staffordwilliams/git/headlines-cdk/node_modules/@aws-cdk/core/lib/duration.ts:228 throw new Error(`'${amount} ${fromUnit}' cannot be converted into a whole number of ${toUnit}.`); ^ Error: '10 seconds' cannot be converted into a whole number of minutes. at convert (/Users/staffordwilliams/git/headlines-cdk/node_modules/@aws-cdk/core/lib/duration.ts:228:11) # serverless error Configuration error at 'functions.parseFeed.events[0].schedule.rate': should match pattern "^rate\((?:1 (?:minute|hour|day)|(?:1\d+|[2-9]\d*) (?:minute|hour|day)s)\)$|^cron\(\S+ \S+ \S+ \S+ \S+ \S+\)$"
Bundling
Serverless Framework uses webpack, requiring a webpack.config.js and the serverless-webpack plugin to compile and bundle our TypeScript. Webpack is a bit complicated though we don’t need to configure it by using the aws-nodejs-typescript template when starting. Webpack is also somewhat slow. lambda-nodejs.NodejsFunction in CDK uses esbuild and while the first invocation takes 2-3 minutes, after this bundling takes about 3 seconds.
CDK ✅
Function deployment
Both frameworks take some time to initially create the stack, but afterwards, when we change a function, we might want to deploy that function several times during the development cycle. CDK does not support single function deployment - the entire stack must be deployed, and timing here will be based on how many resources are in the stack. Serverless Framework supports single function deployment via sls deploy --function myFunction. There are interesting workarounds to decreasing function deploy time in CDK, though in the TypeScript case we’d still need something to transpile to javascript using such a method.
Serverless Framework ✅
Function invocation
CDK has no means to invoke a function - instead we should fallback to the aws-cli. Serverless has sls invoke to invoke a deployed function, and sls invoke local which does not require the function to be deployed. CDK cannot invoke functions locally.
Serverless Framework ✅
Offline mode
Lambdas can be hosted and executed locally using the serverless-offline plugin. CDK does not have any local hosting features.
Serverless Framework ✅
Language
AWS CDK is available in multiple languages including Python, Java, C# and Go. However, there are instances where CDK does not support the latest CloudFormation features and one needs to fallback to writing dynamic CloudFormation within the code - this can be a little annoying in a strongly typed language like C#. Serverless is available in TypeScript/JavaScript only.
CDK ✅
Cloud Provider support
AWS CDK is specific to AWS. Serverless Framework supports many cloud providers including Azure, AWS and Google.
Serverless Framework ✅
Conclusion
CDK’s terser syntax and policy change warnings are great features for infrastructure-laden projects running in AWS. Serverless Framework has a better developer experience for building and deploying functions and supports multiple cloud providers.