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:
npm 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:
cdk 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:
aws sts get-caller-identity
Initialisation
Initialise the project with the following:
mkdir 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
:
export 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:
cdk synthesize
And we can deploy the stack with:
cdk deploy
And you can bring the whole thing back down again with:
cdk destroy
Adding Resources
Adding an S3 Bucket is as simple as adding the following to our HeadlinesCdkStack
class:
const 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:
$ 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.
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);
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.
const 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 deploy
ed, looks like this:
import * 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 ✅
# 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.
# 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.