The Serverless Framework is the most popular framework for building serverless applications on Amazon Web Services (AWS) and other cloud providers. In July 2019, AWS announced its own framework Cloud Development Kit. AWS CDK is a framework to deploy serverless applications and any AWS resource. AWS CDK helps you achieve infrastructure as code similar to AWS CloudFormation and Terraform.

This post will compare Serverless Framework and AWS CDK in different areas: framework ease of use, extensibility, and security.

Overview of the frameworks

Both AWS CDK (which we will refer to as "CDK") and the Serverless Framework (which we will refer to as "Serverless") are both JavaScript frameworks that you may install as a Command Line Interface (CLI) script via npm or yarn. Both support AWS, but their use varies.

What is the Serverless Framework?

Serverless allows you to deploy serverless applications to multiple cloud providers. Serverless supports the following providers:

  • AWS
  • Azure
  • Tencent Cloud
  • Google Cloud
  • Knative
  • Alibaba Cloud
  • Cloudflare
  • fn
  • Kubeless
  • OpenWhisk
  • spotinst

The Serverless configuration file (named "serverless.yml") uses a similar format for most providers, which allows you to switch mental context between providers reasonably easily. The "serverless.yml" file will enable you to specify your configuration using YAML syntax and put your function source code like JavaScript, Python, Go, and any other language the cloud provider supports. Serverless will deserialize the "serverless.yml" and convert it to the cloud provider's underlying format (e.g., AWS CloudFormation template).

What is Amazon Web Services (AWS) Cloud Development Kit (CDK)?

CDK allows you to deploy resources in AWS using TypeScript, JavaScript, Python, Java, and .NET. Your source code defines both the resources and the files those resources need (e.g., AWS Lambda function source code). CDK will synthesize the source code to create the appropriate AWS CloudFormation template.

Let's Compare the Two

1. Setting up a basic project

We will review how to set up a project on AWS for both frameworks.

I will assume you already have an AWS account and know how to create the appropriate IAM credentials or temporary credentials to deploy AWS resources.

I will assume you have Node.js, npm, and your desired programming language installed on your machine.

Serverless Framework project setup

To install Serverless, use npm:

npm install -g serverless

This allows you to use "serverless" or "sls" as a CLI command.

To create a new Serverless project, use the "serverless" or "sls" command and follow the on-screen prompts:

sls

You will find the following starter files in your new project.

$ ls sls-example/
handler.js    serverless.yml

I will prefer the "sls" command throughout and will choose Node.js JavaScript (or TypeScript) as the Lambda function language.

AWS CDK project setup

We need to install the AWS CLI and CDK.

To install the AWS CLI, follow the AWS CLI installation instructions since they vary per operating system

To install CDK, use npm:

npm install -g aws-cdk

To create a new CDK project, use the "cdk" command.

mkdir cdk-example
cd cdk-example
cdk init app --language=javascript

You will find the following starter files in your new project.

$ ls
README.md    lib                  package.json
bin          node_modules         test
cdk.json     package-lock.json

I will prefer the Node.js JavaScript as the Lambda function language. I recommend you consider TypeScript over JavaScript since CDK was built with TypeScript. I am choosing Node for the comparison to Serverless.

Comparison

Both are pretty easy to set up the basic project structure.

2. Ease of use

I would argue each framework is relatively easy to use. You might prefer one framework over another depending on whether you like writing code and depending on your understanding and knowledge of AWS.

2.1. Deploying a Lambda function

We will start with a simple use case.

Serverless Framework

Let's suppose you only want to deploy a Lambda function. The previous project set up already has one Lambda function ready to deploy.

# serverless.yml
# automatically created file
# no code changes made

service: sls-example

provider:
  name: aws
  runtime: nodejs12.x

functions:
  hello:
    handler: handler.hello

Serverless will assume you want a dev stage and the us-east-1 region.

/* handler.js */
/* automatically created file */
/* no code changes made */

'use strict';
module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };
};

Now we just need to deploy the configuration.

sls deploy --aws-profile my_aws_profile

Serverless will create a CloudFormation template that deploys the following items:

  • S3 bucket that contains the Serverless configuration files and the appropriate S3 bucket policy
  • One Lambda function
  • The latest Lambda function version
  • One CloudWatch Logs log group to capture the function logs
  • The IAM role that Lambda function will use to access any AWS services and resources
The resources that were created by the AWS CloudFormation template generated from the Serverless Framework.

A small number of lines in the "serverless.yml" file deployed the "handler.js" function code and set up all the necessary supporting resources. Furthemore, the resource naming convention follows a naming pattern. Not bad!

AWS CDK

The project (i.e., app) structure we previously created has no resources. We will need to add the desired resources.

We will need to install an additional package to define the Lambda in our app.

To install the additional package, run the following npm command:

npm i --save @aws-cdk/aws-lambda

We can now define a Lambda function in our app by modifying the existing code.

We do not need to make changes to the bin/cdk-example.js file.

#!/usr/bin/env node

/* bin/cdk-example.js */
/* automatically created file */
/* no code changes made */

const cdk = require('@aws-cdk/core');
const { CdkExampleStack } = require('../lib/cdk-example-stack');

const app = new cdk.App();
new CdkExampleStack(app, 'CdkExampleStack');

We need to add the Lambda function to the lib/cdk-example-stack.js file.

/* lib/cdk-example-stack.js */
/* automatically created file */
/* code changes made */

'use strict';

const cdk = require('@aws-cdk/core');

/* addition */
const lambda = require('@aws-cdk/aws-lambda');
const fs = require('fs');
/* end addition */

class CdkExampleStack extends cdk.Stack {
  /**
   *
   * @param {cdk.Construct} scope
   * @param {string} id
   * @param {cdk.StackProps=} props
   */
  constructor(scope, id, props) {
    super(scope, id, props);
    /* addition */
    const helloSrc = fs.readFileSync('handler.js').toString();
    const helloLambda = new lambda.Function(this, 'HelloLambda', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'handler.hello',
      code: new lambda.InlineCode(helloSrc, {
        encoding: 'utf-8',
      }),
    });
    /* end addition */
  }
}

module.exports = { CdkExampleStack };

We will use the same handler.js file from the Serverless example and add it to the cdk-example folder.

We are ready to deploy. Let's run the CDK CLI commands.

# check for errors
cdk synth
# deploy
cdk deploy --profile my_aws_profile

CDK will use the default region in your AWS CLI configuration or us-east-1 if there is no default region.

CDK will create a CloudFormation template that deploys the following items:

  • CDK metadata
  • One Lambda function
  • The IAM role that Lambda function will use to access any AWS services and resources

What happened to the S3 bucket, S3 bucket policy, latest Lambda function version, and CloudWatch Logs log group that Serverless deploys?

CDK uses CDK metadata rather than uploading the CloudFormation template and the other resources Serverless needs to an S3 bucket and creating the appropriate bucket policy. We get a security bonus because we cannot access the CDK configuration from the AWS console. Nice!

Serverless assumes you want a version of the Lambda function whenever you update it. This might be a useful feature, but, in most cases, you will probably only need the latest version. CDK does not make that assumption for you. If you want versioned functions, you need to specify it.

AWS will automatically create a CloudWatch Logs log group when the Lambda triggers the first time. We, therefore, do not need to specify it in our app. But, CDK or CloudFormation will not automatically delete the log group when deleting the stack since it's not part of the configuration. If we want that to happen, we need to add the log group in the lib/cdk-example-stack.js file.

CDK assumes you want to use the best practices and trusted patterns. It, therefore, created the IAM role for the function.

Comparison

Serverless made it super simple to create a Lambda function and the supporting resources. But, it does create a new S3 bucket to hold the Serverless configuration.

CDK required a little extra effort because it did not come with a sample Lambda function. I had to read the API reference to determine how to add a Lambda. Furthermore, it did not deploy all the resources I would have wanted (e.g., the CloudWatch Logs log group). But, it limited the number of resources and gave me more control.

2.2. Adding a DynamoDB table

A Lambda function more than likely will need to get data from a database. We will use DynamoDB, which is a serverless database table service.

Serverless Framework

Serverless does not support certain resources (e.g., DynamoDB tables and S3 buckets) out of the box. We therefore need to resort to using CloudFormation to create them.

We will append the CloudFormation template code to the existing serverless.yml configuration file.

# serverless.yml

# additions
resources:
  Resources:
    TableUsers:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-${self:provider.stage}-users
        AttributeDefinitions:
          - AttributeName: PartitionKey
            AttributeType: S
          - AttributeName: SortKey
            AttributeType: S
        KeySchema:
          - AttributeName: PartitionKey
            KeyType: HASH
          - AttributeName: SortKey
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
# end additions

When we deploy again, we will see a new resource for the DynamoDB table.

We will want our Lambda function to be able to read data from the table. We will need to add an IAM policy to the serverless.yml file.

# serverless.yml

# addition
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "dynamodb:GetItem"
      Resource:
        Fn::Join:
          - ""
          - - "arn:aws:dynamodb:"
            - "Ref" : "AWS::Region"
            - ":"
            - "Ref" : "AWS::AccountId"
            - ":table/"
            - "Ref" : "DdbTableHello"
# end addition

When we deploy it, will not see any new resources. We can go to the IamRoleLambdaExecution IAM role resource and see it have the IAM policy we just added.

        {
            "Action": [
                "dynamodb:GetItem"
            ],
            "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/sls-example-dev-hello",
            "Effect": "Allow"
        }

It was quite a bit of effort to add the DynamoDB table and give the Lambda function permission to get an item from it.

AWS CDK

We need to add an additional package to add DynamoDB to our app.

npm i --save @aws-cdk/aws-dynamodb

We need to update the lib/cdk-example-stack.js file to add the DynamoDB resource. I will limit the code shown to focus on the changes.

/* lib/cdk-example-stack.js */
/* automatically created file */
/* code changes made */

'use strict';

const cdk = require('@aws-cdk/core');

/* addition */
/* end addition */

/* addition 2 */
const dynamodb = require('@aws-cdk/aws-dynamodb');
/* end addition 2 */

class CdkExampleStack extends cdk.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);
    /* addition */
    /* end addition */

    /* addition 2 */
    const helloTable = new dynamodb.Table(this, 'HelloTable', {
      partitionKey: {
        name: 'PartitionKey',
        type: dynamodb.AttributeType.STRING,
        billingMode: dynamodb.BillingMode.PROVISIONED,
      },
      sortKey: { name: 'SortKey', type: dynamodb.AttributeType.STRING },
      readCapacity: 1,
      writeCapacity: 1,
    });
    helloTable.grantReadData(helloLambda);
    /* end addition 2 */
  }
}

module.exports = { CdkExampleStack };

When we synthesize and deploy, we will find two new resources:

  • One DynamoDB table
  • One IAM policy

The IAM policy will show the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:Query",
                "dynamodb:GetItem",
                "dynamodb:Scan"
            ],
            "Resource": [
                "arn:aws:dynamodb:us-east-1:123456789012:table/CdkExampleStack-HelloTable2C0887DE-1D2AVL6SE753L"
            ],
            "Effect": "Allow"
        }
    ]
}

You may notice this IAM policy has more allowed actions than the one we created with Serverless. The IAM policy we created with Serverless only contained the policy we defined in the serverless.yml file. Whereas, we used the convenient grantReadData function to automatically create all the read permissions the function will ever need. We could have manually created the IAM policy and add it to the role. Although the IAM policy we created with CDK is technically not least privileged, the intent is least privileged. Doing a read by getting one item vs. getting multiple items is still a read nonetheless. If we wanted the actual least privilege, we would need to add more code to the file.

Comparison

Serverless did not simplify our creation of the DynamoDB table the IAM policy. We would attach the IAM role policy assigned to the Lambda function. We had to resort to using native CloudFormation template code and IAM policy statements.

CDK simplified the DynamoDB and IAM policy creation with a few code lines and without knowing the CloudFormation template code and IAM policy statements. We received the benefit of getting all the privileges the Lambda function would need to perform a read. Still, it may not be least privileged if we truly wanted the function only to get one read. We would then need to add more code to add the appropriate IAM policy and lose its simplicity.

3. Extensibility

In this section, we will review the extensibility of CDK and Serverless.

CDK

CDK uses constructs and defines them in three levels. Level 1 allows you to work directly with CloudFormation. Level 2 provides an abstraction and human-readable code; see the examples above. Level 3 will enable you to combine Level 1 and Level 2 constructs with building reusable patterns (e.g., deploying an S3 bucket and CloudFront distribution to host a static web site).

AWS has built several Level 3 constructs to solve many common patterns. They distribute under their AWS Solutions Constructs. For example, you can install their construct to deploy an S3 bucket and CloudFront distribution.

npm i --save @aws-solutions-constructs/aws-cloudfront-s3

We will create a `lib/cloudfront-s3.js` file to define our new stack.

/* lib/cloudfront-s3.js */
/* new file */

'use strict';

const cdk = require('@aws-cdk/core');
const {
  CloudFrontToS3,
} = require('@aws-solutions-constructs/aws-cloudfront-s3');

class CloudFrontS3Stack extends cdk.Stack {
  /**
   *
   * @param {cdk.Construct} scope
   * @param {string} id
   * @param {cdk.StackProps=} props
   */
  constructor(scope, id, props) {
    super(scope, id, props);
    new CloudFrontToS3(stack, 'cloudfront-s3', {});
  }
}

module.exports = { CloudFrontS3Stack };

Now we add that stack to our app.

#!/usr/bin/env node
/* bin/cdk-example.js */
/* automatically created file */
/* code changes made */

'use strict';

const cdk = require('@aws-cdk/core');
const { CdkExampleStack } = require('../lib/cdk-example-stack');
const { CloudFrontS3Stack } = require('../lib/cloudfront-s3-stack');

const app = new cdk.App();
new CdkExampleStack(app, 'CdkExampleStack');
new CloudFrontS3Stack(app, 'CloudFrontS3Stack');

When we deploy, CDK automatically creates the S3 bucket, CloudFront distribution, and CloudFront origin access identity that allows CloudFront to distribute the S3 objects. We will need to upload our files to the S3 bucket and create a CloudFront invalidation to clear the CloudFront cache. We will still need to create the Route 53 hosted zone, Certificate Manager certificate, and Route 53 A alias record to the CloudFront distribution to have a fully working web site.

CDK also supports building and using plugins. Plugins modify the CDK deploy process, whereas constructs allow us to build reusable patterns. For example, the previous Level 3 construct allowed us to deploy multiple CloudFront-S3 patterns. In contrast, the cdk-multiple-profile-plugin will enable us to deploy to using multiple AWS profiles (which could be multiple AWS accounts).

Serverless

Serverless has many open-source plugins that extend the deployment process and even allow us to extend how it processes the serverless.yml file.

For example, we can deploy a static web site. The fullstack-serverless plugin allows us to deploy the S3 bucket, CloudFront distribution, and CloudFront origin access identity, run the build command for the web site, and upload the files to the S3 bucket.

We install the plugin.

npm install --save-dev fullstack-serverless

We add the configuration to the serverless.yml file.

# serverless.yml
# only some of the options are displayed

plugins:
  - fullstack-serverless

custom:
  fullstack:
    domain: my-custom-domain.com
    certificate: arn:aws:acm:us-east-1:...
    bucketName: my-website
    distributionFolder: websiteDir/dist
    indexDocument: index.html
    errorDocument: error.html
    singlePageApp: true
    compressWebContent: true
    clientCommand: npm run build
    clientSrcPath: websiteDir

When we deploy, we will have a lot of the steps completed. We will still need to create the Route 53 hosted zone, Certificate Manager certificate, and Route 53 A alias record to the CloudFront distribution to have a fully working web site.

Comparison

Both CDK and Serverless support plugins. CDK provides Level 3 constructs to build patterns. In contrast, Serverless does not have this concept aside from making a plugin that allows you to modify the serverless.yml file that specifies the build patterns. Serverless is a stable and well-established framework with multiple plugins. CDK is slightly over one year old, and some of its Level 2 constructs and Level 3 constructs are still experimental. The experimental constructs might introduce breaking changes in future releases. Of course, there are no guarantees that Serverless plugins will not introduce breaking changes as they are maintained outside of the control of Serverless, Inc.

4. Stability

I would argue both Serverless and CDK are stable. Serverless, Inc. manages Serverless, and they follow established software development processes. AWS manages CDK and also follow established software development processes.

The Serverless Framework is stable, and they provide backward compatibility with their current V1 and its minor releases. CDK itself is stable, and so are all the Level 1 constructs I have reviewed. This means you can reliably build a CDK app using CloudFormation settings without worrying about breaking changes.

As mentioned earlier, some of the CDK Level 2 and Level 3 constructs are experimental, and Serverless plugins may or may not be stable. Essentially, the potential for breaking changes comes from leveraging conveniences built on top of the base functionality. CDK's advantage over Serverless is that AWS is building and maintaining Level 2 and Level 3 constructs, which gives AWS more stability over the entire ecosystem. In contrast, Serverless has no direct control over the plugins.

5. Security considerations

5.1. Required AWS resources to deploy

Serverless requires an S3 bucket that contains the Serverless configuration files. If you deploy a Serverless configuration with no resources specified in the serverless.yml file, it will still create that S3 bucket.

CDK does create an S3 bucket to host its configuration data. If you deploy an empty CDK app, it will generate a CDK metadata resource that is not accessible via the AWS console or CLI.

5.2. Resource configurations

Serverless assumes some resources it believes you may want (e.g., Lambda versioning, CloudWatch Logs log groups and one IAM role for all your functions). The bucket is not Internet-accessible, but it is not configured as a private bucket.

CDK tries not to assume the resources you want and gives you most of the control (e.g., it will not create the Lambda versioning and CloudWatch Logs log groups, and it will not create one IAM role for all your functions). Suppose you specify a Lambda function needs a package that contains all its dependencies. In that case, it will automatically create the S3 bucket it needs to deploy the package to the Lambda function. Still, it configures it as a private bucket. As you saw in the example above, CDK did not create the S3 bucket because the Lambda function code was inline code rather than a package.

5.3. Principle of least privilege

The CDK Level 2 constructs provide the convenience of granting privileges using "grant" functions. These "grant" functions create the appropriate least-privileged IAM roles (e.g., it adds all the IAM policy statements that allow read access between the two specific resources and does not add any create/delete/update permissions).

Serverless assumes you want one IAM role for all your functions. This works fine when you have one function per stack, but might start to violate the principle of least privilege when you add more functions. You will need a plugin to help you define IAM roles for each function, or avoid using the built-in IAM roles and manually define them with a CloudFormation template in the "resources" section of the "serverless.yml" file.

We are only using AWS. Which should we use?

Both Serverless and CDK ultimately build a CloudFormation template, but how it creates the template differs. In some respects, Serverless makes deploying serverless applications easier, while in other regards, it requires more work. CDK requires a better understanding of AWS and gives you more control. When it comes to deploying resources other than Lambda functions, CDK needs less code than Serverless (unless you are using Serverless plugins).

If you are new to AWS and serverless computing and want to start deploying a quickly as possible, I recommend starting with Serverless.

Suppose you are new to AWS and want to learn how AWS works. In that case, I suggest watching YouTube videos about the AWS Certified Cloud Practitioner certification exam to understand AWS. When you have a basic understanding of how AWS works, Serverless is still probably the best starting point. Still, you will have sufficient knowledge to start using CDK.

Suppose you have moderate to advanced knowledge of AWS. In that case, CDK will provide you with more control to achieve any simple or complex deployment you wish to achieve. Serverless will also help you achieve the same outcome, but you may need to build your own AWS CloudFormation templates. If you like the simplicity of the YAML configuration, you might want to use Serverless.

Suppose you are more of a DevOps engineer rather than a software developer. In that case, you might lean toward Serverless, but writing the source code for CDK is probably not out of your reach.

View the source code at https://github.com/miguel-a-calles-mba/secjuice/tree/master/cdk-vs-sls.

A Note from the Author

Want to level up in your knowledge of serverless security?

Join my mailing list to receive updates about an upcoming book on serverless security, my writings, and cybersecurity news.

Visit https://goo.gl/forms/mtdRcj3vDJF3qkGo1 and sign up.

Stay secure, Miguel

View my linkedIn profile

The awesome image used in this article is called Alchemist and was created by Ryogo Toyoda.