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.

solution architecture diagram

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 deployed, 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.