ℹ️ Edit: An updated version, with more options included, exists here
⚡️Serverless Frameworks for 2023
Sebastian Bille for AWS Community Builders ・ Jan 23 '23
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.
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.toml
file 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:
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
- Enables sharing & re-use of components
- Large & helpful community
- Makes the infrastructure testable
- You can (likely) use the same programming language to define the infrastructure of your application as your actual application
➖ 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
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!
Top comments (14)
Thanks @tastefulelk
I was looking a practical difference between these three to start my first serverless project and stumbled upon this article which very well written. It clearly explains the difference.
Thank you Aayush, I'm glad it helped!
Yo Sebastian, nice write up!
When I was getting into this whole area, understanding the differences between the frameworks was a challenge (I did a write up back then, too), and they're not entirely direct competitors which makes it a bit tricky. I never got into Serverless Framework... as you say jumping between custom configurations and CloudFormation feels weird and clunky. I've enjoyed using SAM recently, but also for the out-of-the-box local dev set up. I've still not yet jumped into CDK so far, but I keep hearing great things, so maybe it's time to stop pushing it down my priorities list!
Thank you Lou! 🙌
You should! If nothing else, it broadens the horizon and I enjoyed playing around and learning it since it's a quite different approach to the challenges!
I would like to Introduce one more framework to this list
somod.dev
Features:
CONS:
How could we compare the above options with
serverless-stack.com/
Great question, I'm actually working on an updated version of this post where I include SST! I'll let you know here when I post it!
Thanks a ton and looking forward to the updated one!!
Meanwhile, couple of suggestions -
Good luck!
FYI we are working on Lift (github.com/getlift/lift) at Serverless, it seems that in this specific example this could help :)
In any case that was a fair comparison, nice article!
That's true, it probably would've! Lift is pretty much a counter to the point that you often have to resort to CloudFormation as soon as you want anything more than a Lambda function 👍
Thank you for such a great article :)
Sometimes, I use the cdk and serverless in a single project.
Thank you Sophy! 🙌
Thank you for sharing difference between these tools. It was very helpful to understand the cons and pro.
Well written.
Thanks a lot for the feedback Augusto! ❤️