Serverless Framework vs SAM vs AWS CDK

Serverless Framework vs SAM vs AWS CDK

ℹ️ Edit: An updated version, with more options included, exists here.


When building serverless apps on AWS today there's a couple of different toolkits available that helps you develop, test and deploy your project. Serverless Framework was the king for a long time but both AWS SAM and the CDK have been increasing in popularity lately. But which one is the best to use in a new project and what's the difference between them anyway. After all, they're all just tools to produce Cloudformation templates anyway, right?

To get an understanding of the strengths and disadvantages of each option, I decided to build an identical example application across all three and compare the approaches.

By the end of this post, I hope you'll have a basic understanding of the Serverless Framework, AWS SAM, and the CDK and that you'll be able to make an educated choice on what'll suit your next project best based on your needs and preferences.

Our Example Application

To keep it interesting, the app we're using to showcase each framework certainly isn't your typical ToDo-app - it's a ToDont-app. A user can send a POST request to an API Gateway describing something they really shouldn't do, a Lambda function takes the ToDont-item and puts it on an SQS queue that acts as a buffer before finally another Lambda function consumes the buffer queue, pretends to do some heavy processing on the item and persists it in a DynamoDB table.

Web App Reference Architecture (3)

The application architecture is simple enough to easily comprehend but "complex" enough to resemble an actual app. To keep the code compact and readable, best practices and common sense have sometimes had to be omitted. All configs are complete and fully functional however and if you want to play around with the examples and deploy the apps yourself, you can find the code and the full examples here.

Our POST Lambda function looks like this

// src/post.js
const { SQS } = require('@aws-sdk/client-sqs');

const sqs = new SQS();

const handler = async (event) => {
  console.log('event', event);
  const { id, title } = JSON.parse(event.body);

  await sqs.sendMessage({
    QueueUrl: process.env.QUEUE_URL,
    MessageBody: JSON.stringify({
      id,
      title,
    })
  });

  return {
    statusCode: '200',
  };
};

module.exports = { handler };

the Process Lambda looks like this

// src/process.js
const { DynamoDB } = require('@aws-sdk/client-dynamodb');
const { marshall } = require("@aws-sdk/util-dynamodb");

const ddb = new DynamoDB();

const handler = async (event) => {
  console.log('event', event);

  const tasks = event.Records.map((record) => {
    const { id, title } = JSON.parse(record.body);
    return ddb.putItem({
      TableName: process.env.TABLE_NAME,
      Item: marshall({
        title,
        id,
      }),
    });
  });

  return Promise.all(tasks);
};

module.exports = { handler };

Prerequisites

If you want to follow along and deploy the apps, please note the following:

  • Each of the comparisons below assumes that you've installed the following packages as dependencies in your project

    • @aws-sdk/client-dynamodb

    • @aws-sdk/util-dynamodb

    • @aws-sdk/client-sqs

  • While Yarn is used as the package manager & script runner below you could of course use NPM instead with the corresponding commands.

  • All of the examples assume that you've got an AWS credentials default profile configured

Serverless Framework

Serverless Framework ("Serverless" below) has been around for a long time now and has long been the preferred framework for a large part of the community. It's a simple tool that abstracts away and simplifies many of the nastier parts of CloudFormation and comes packed with features to simplify testing and deployment of your app.

The preferred way to run the Serverless CLI is to install it as a (dev)dependency in your project by running yarn add serverless -D and then all that's missing is a serverless.yml file which is used to define your application and its infrastructure. You can find the full configuration reference here but in short, the serverless.yml consists of two parts:

  • Your Serverless Framework configuration is used to describe your application stack, AWS environment, and lambda functions

  • Any additional infrastructure defined as CloudFormation resources, such as our DynamoDB table and SQS queue.

Here's how the serverless.yml for our application looks.

// serverless.yml
service: sls-todont

provider:
  name: aws
  region: eu-north-1
  runtime: nodejs14.x
  environment: # Inject environment variables
    TABLE_NAME: ${self:custom.tableName}
    QUEUE_URL: !Ref todontsQueue
  iamRoleStatements: # Configure IAM role statements
    - Effect: Allow
      Action: sqs:sendMessage
      Resource: ${self:custom.queueArn}
    - Effect: Allow
      Action: dynamodb:putItem
      Resource: ${self:custom.tableArn}

custom: # Custom variables that we can reference elsewhere
  tableName: ${self:service}-table
  queueName: ${self:service}-queue
  tableArn: # Get ARN of table with CloudFormation helper
    Fn::GetAtt: [todontsTable, Arn]
  queueArn: # Get ARN of queue with CloudFormation helper
    Fn::GetAtt: [todontsQueue, Arn]

functions: # Define our two Lambda functions
  post:
    handler: src/post.handler
    events: # Invoke on post requests to /todonts
      - http:
          method: post
          path: todonts
  process:
    handler: src/process.handler
    events: # Consume SQS queue
      - sqs:
          arn: ${self:custom.queueArn}

# CloudFormation below to define our infrastructure resources
resources: 
  Resources:
    todontsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: 'PAY_PER_REQUEST'
    todontsQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:custom.queueName}

Now to deploy our application, all we need to do is run yarn serverless deploy.

The Serverless CLI includes some utility features that can be used to print or tail the logs of a deployed function by running yarn serverless logs --function process [--tail] or even invoke the function with yarn serverless invoke --function process. Most of the time during development, however, you're not going to be invoking the function. Instead, you'll let Serverless emulate and run the functions locally and you can do that by running yarn serverless invoke local --function post.

Pros

  • Large & helpful community

  • The plugin ecosystem

  • Simple configuration with neat variable support

  • Great debugging and testing utilities

Cons

  • Most apps will need to resort to CloudFormation definitions for some parts of the infrastructure

  • Hard to share configuration and components

  • Only YAML configurations. It's technically supported to write the configuration in JS but the documentation for it is close to non-existent

  • I've seen a lot of devs struggle with understanding where the line between Serverless configuration and CloudFormation configuration actually or why they have to change the syntax in the middle of the file

Resources:

Get started with Serverless Framework

Serverless Stack tutorial for deplying a production Serverless app

AWS SAM

Much like Serverless Framework, SAM (or the Serverless Application Model) is a combination of an abstraction layer to simplify CloudFormation and a CLI with utilities to test and deploy your app.

Here's the official install instructions for the SAM CLI which is installed globally on your system. SAM uses a samconfig.toml file to describe information about your app, such as the name and where and how it should be deployed, and a template.yml file to describe the actual resources your app will use.

The template.yml format follows the CloudFormation template anatomy templates but with a few added fields. Let's have a look:

// template.yml
# Boilerplate to identify template as SAM template
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: sam-todonts

Globals:
  Function:
    Runtime: nodejs14.x
    Environment: # Inject environment variables
      Variables:
        QUEUE_URL: 
          Ref: TodontsQueue
        TABLE_NAME:
          Ref: TodontsTable

Parameters: # Parameters which can be filled by the CLI on deploy
  TableName: 
    Description: Name of DynamoDB table
    Type: String
    Default: sam-todonts-table
  QueueName:
    Description: Name of SQS queue
    Type: String
    Default: sam-todonts-queue

Resources: 
  PostFunction:
    Type: AWS::Serverless::Function
    FunctionName: sam-todonts-post
    Properties:
      Handler: src/post.handler
      Events:
        Post: #  Invoke on post requests to /todonts
          Type: HttpApi
          Properties:
            Path: /todonts
            Method: post
      Policies:
        - SQSSendMessagePolicy: # Use predefined IAM policy
            QueueName:
              Fn::GetAtt: [TodontsQueue, QueueName]

  ProcessFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/process.handler
      Events:  # Consume SQS queue
        SQSQueueEvent:
          Type: SQS
          Properties:
            Queue:
              Fn::GetAtt: [TodontsQueue, Arn]
      Policies: # Use predefined IAM policy
        - DynamoDBWritePolicy:
            TableName:
              Ref: TodontsTable

  TodontsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: sam-todonts-table
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  TodontsQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: sam-todonts-queue

The configuration is a bit verbose but luckily the CLI can help you with a place to start by running sam init and answering a few questions about what your planning to build.

We can generate the samconfig.tomlfile and deploy at the same time by running sam deploy --guided.

Again, much like the Serverless Framework CLI, SAM comes loaded with utility features to test and debug your app. sam local invoke [functionName] can be used to run a Lambda function, or you can start a local HTTP server that hosts your function by running sam local start-api. You can also easily fetch the logs from a deployed function by running sam logs --name [functionName].

The great thing about separating the definition of the app and how the app is built in two different files is that the template.yml file can be written very generically so that it can be shared and re-used, you'll just have a different samconfig.toml in each project. SAM also integrates very well with CodeBuild to enable blue-green deployments.

Pros

  • Enables sharing & re-use of templates

  • Well integrated with AWS build pipelines

  • Great debugging and testing utilities

  • Can be combined with CDK

Cons

  • Verbose configuration

  • CLI is missing some features you'd expect, such as tearing down a deployed app.

Resources:

Getting started with AWS SAM

Serverless Application Repository

Serverless Patterns Collection

AWS CDK

The AWS Cloud Development Kit (CDK) isn't purely a tool for creating serverless apps, rather it's a full-blown infrastructure-as-code framework that allows you to use code, not config, to define your application.

You can install the CDK CLI by running yarn global add aws-cdk and then generate a starter project by running cdk init app --language --language typescript. There's a bunch of project configuration files and boilerplate that's generated when you run the init command but let's have a look at how the lib/cdk-stack.ts file looks like after we've described our ToDont-app in it.

// lib/cdk-stack.ts
import * as cdk from '@aws-cdk/core';
import lambda = require('@aws-cdk/aws-lambda-nodejs');
import sqs = require('@aws-cdk/aws-sqs');
import dynamodb = require('@aws-cdk/aws-dynamodb');
import { ApiEventSource, SqsEventSource } from '@aws-cdk/aws-lambda-event-sources';
import { Runtime } from '@aws-cdk/aws-lambda';

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // define our DynamoDB table
    const dynamoTable = new dynamodb.Table(this, 'cdk-todonts-table', {
      tableName: 'cdk-todonts-table',
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
    });

    // define our SQS buffer queue
    const sqsBuffer = new sqs.Queue(this, 'cdk-todonts-queue', {
      queueName: 'cdk-todonts-queue',
    });

    // define our processing lambda
    const processLambda = new lambda.NodejsFunction(this, 'cdk-todonts-process', {
      runtime: Runtime.NODEJS_14_X,
      handler: 'handler',
      entry: 'src/process.js',
      events: [new SqsEventSource(sqsBuffer)],
      environment: {
        TABLE_NAME: dynamoTable.tableName
      }
    });

    // grant write access for the processing lambda to our dynamo table
    dynamoTable.grantWriteData(processLambda);

    // define the lambda backing our API
    const postLambda = new lambda.NodejsFunction(this, 'cdk-todonts-post', {
      runtime: Runtime.NODEJS_14_X,
      entry: 'src/post.js',
      handler: 'handler',
      events: [new ApiEventSource('POST', '/todonts')],
      environment: {
        QUEUE_URL: sqsBuffer.queueUrl,
      }
    });

    // grant write access to the SQS buffer queue for our API lambda
    sqsBuffer.grantSendMessages(postLambda);
  }
}

The basic building blocks of a CDK application are called constructs which represent a "cloud component", whether that's a single service instance such as an SQS Queue or a set of services encapsulated in a component. Constructs can then be shared and reused between projects and there's a fantastic community that has built a massive collection of high-quality components for you to use. Having the app and its infrastructure described fully in code also means that we can write actual tests against our setup - pretty darn cool, huh?

Before we can deploy the app for the first app, we need to bootstrap the AWS environment (account & region combination) to provision some resources that the CDK uses to deploy the app. After that, we can run cdk deploy to deploy our application.

The CDK CLI doesn't bring the same utility around testing and debugging as SAM and Serverless does but it is possible to use the SAM CLI together with the CDK to help bridge the gap. There's also a newcomer on the block, Serverless-Stack, an extension of the CDK, that brings a lot of testing utility and serverless specific constructs.

Pros

Cons

  • Need to use another tool, such as SAM or the AWS CLI, if you want to invoke or print the logs of a deployed function.

Resources:

Getting started with the AWS CDK

CDK Patterns

CDK Day

Wrapping Up

There's a lot that's happening in this space at the moment and while I think these are the three most prominent players right now, alternatives popping up left and right. Each framework has its own strengths and benefits and there's rarely a wrong or right when choosing which one will work best in your project.

Please let me know in the comments which one is your favorite and why!

If you enjoyed this post and want to see more, follow me on Twitter at @TastefulElk where I frequently write about serverless tech, AWS, and developer productivity!

Did you find this article valuable?

Support Sebastian Bille by becoming a sponsor. Any amount is appreciated!