⚡️Serverless Frameworks for 2023
In this post, we'll look at some of the most popular frameworks for building serverless applications on AWS, including their features, strengths, and weaknesses. We'll also look at examples of how each framework can be used to build an example application. Whether you're a seasoned serverless developer or are just getting started with it, this post will give you a good overview of the different frameworks available and help you decide which one is best for your needs.
Example Application: A To-Don’t app
To showcase how each framework is used, we'll build a To-Don't application which, obviously, is an app to list things you don't want to do, rather than a To-Do app.
The architecture will look like this:
An API Gateway with a POST /item method
A “Persist” Lambda function that writes the item to a DynamoDB table
A DynamoDB table with Streams enabled
A “Fan-out” function that subscribes to the DynamoDB stream and then fans out the item to an SNS Topic
Implementing an endpoint to fetch items is left as an exercise for the reader; we're on a tight deadline here.
ℹ️ tl;dr: If you want to skip ahead and just compare the structure and configuration for each project, including the code for the actual Lambda functions, you can find them all here.
Serverless Framework
Serverless Framework is an open-source tool built specifically to simplify building serverless apps. While it does support other clouds, such as Azure and Google Cloud, AWS is definitely where the focus is. It's found a carefully balanced way of abstracting away “just enough” of the parts you may not care much about while allowing you the flexibility of CloudFormation when you need it. For a long time, Serverless Framework was the clear choice for many development teams.
It's got a plugin system with wide community support, and it's unlikely there isn't a plugin that solves your problem if you're lacking something from the base framework.
Lately, however, the innovation speed seems to have dwindled, and its competitors have far outrun it in feature additions.
All in all, Serverless Framework is a solid choice - a safe choice, really - but this one won't wow you.
🛠 Working with Serverless Framework
To install the Serverless Framework CLI tool, run npm install -g serverless
. You can then initialize a new project by running serverless,
and you'll get to choose from a number of starting points and examples.
Serverless Framework apps use a serverless.yml
file that, in its simplest form, looks a little something like this:
# name of our app
service: to-dont-app
# let the provider know that we want to deploy to AWS
provider:
name: aws
# define out Lambda functions
functions:
hello:
handler: hello.handler
runtime: nodejs16.x
ℹ️ Since Serverless Framework supports clouds other than AWS, we need to instruct the provider that we want to deploy to AWS.
Given the above serverless.yml
and AWS credentials configured, running serverless deploy
will deploy a ‘hello’ Lambda function. The CLI also includes a bunch of utility functions that help you skip a couple of trips to the AWS console. You can, for example, invoke your newly deployed function by running serverless invoke -f hello
, tail the CloudWatch logs by running serverless logs -f hello --tail
, or generate test events, such as an API Gateway event, by running serverless generate-event -t aws:apiGateway
.
👷♂️ To-Don't example
Our To-Don't application's serverless.yml
looks like this, with added comments explaining what's going on:
service: to-dont
plugins:
# Add a plugin that allows us to specify more fine grained IAM policies
- serverless-iam-roles-per-function
# require serverless v3
frameworkVersion: '3'
provider:
name: aws
# default config for all functions
runtime: nodejs16.x
region: eu-north-1
functions:
persist:
handler: src/persist.handler
# Set an environment variable with the DynamoDB table name,
# based on the reference to the table in the CloudFormation
# Resources block below
environment:
TABLE_NAME: !Ref DynamoTable
# Expose the function for POST requests on /item in an ApiGateway
events:
- http:
path: /item
method: post
# Allow the function to write to our DynamoDB table
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource:
- !GetAtt DynamoTable.Arn
fanout:
handler: src/fanout.handler
# Set an environment variable with the SNS topic ARN
environment:
TOPIC_ARN: !Ref SnsTopic
# Trigger the function when a new item is added to the DynamoDB
# table by listening to the DynamoDB stream
events:
- stream:
type: dynamodb
arn:
!GetAtt DynamoTable.StreamArn
iamRoleStatements:
- Effect: Allow
Action:
- sns:Publish
Resource:
- !Ref SnsTopic
# DynamoDB tables and SNS Topics has to be set up in CloudFormation,
# and the Resources block below allows us to do just that
resources:
Resources:
DynamoTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
SnsTopic:
Type: AWS::SNS::Topic
You can find the full Serverless Framework version of our To-Dont app here.
⚖️ Pros and Cons
Pros:
Strikes a great balance between abstracting away messy CloudFormation code that you likely don't care about and giving you the flexibility to use it when needed.
The plugin ecosystem is fantastic, and you'll most likely find whatever you need.
Battle-tested and proven for years.
Cons:
The configuration can be confusing on what is Serverless Framework config and, what is actual CloudFormation, and how they interact.
Innovation speed and support have dwindled lately.
Not possible to define re-usable components across stacks
📚 Resources
Get started with Serverless Framework
Lightning fast & simple Typescript Serverless builds
6 Serverless CLI commands you didn't know existed
Serverless Framework Serverless Patterns Collection
AWS SAM
AWS's open-source take on a declarative framework specifically for deploying serverless applications is called AWS SAM, or Serverless Application Model. It had a slow start but has had an incredible cadence of pumping out features and quality-of-life additions in the last year or two and is now a very serious contender.
It uses a superset of CloudFormation, which should make anyone familiar with that should feel right at home, and, much like Serverless Framework, it aims to abstract away the ugly parts and streamline the development process.
SAM is an excellent choice for building your serverless apps; it's got great support from AWS, they're active in the community to take in feedback and ideas, and they churn out features like there's no tomorrow. The configuration is however a bit verbose, which might tilt the learning curve slightly for beginners.
🛠 Working with AWS SAM
To install the SAM CLI, follow the installation instructions for your platform here, or if you use Homebrew, run:
brew tap aws/tap
brew install aws-sam-cli
Bootstrapping a new project with SAM is done by running `sam init` and, again, you'll get to choose from a couple of different starting points.
SAM, since it's a superset of CloudFormation, uses a template.yaml
file for its configuration, and a simple example might look like this:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/app.lambdaHandler
Runtime: nodejs16.x
SAM also optionally uses a samconfig.toml
file that specifies default parameters for its commands, such as what region you want to deploy to or the stack name. The great thing about separating the definition of the app and how the app is built in two different files is the template.yaml
file can be written very generically so that it can be shared and re-used.
Given the above template.yaml
and AWS credentials configured, running sam deploy --guided
will give you a wizard that will automatically create the samconfig.toml
file for you and then deploy the application and our ‘hello’ Lambda function.
The SAM CLI also includes helpful utility functions to make your life easier. Unfortunately, you can't invoke the deployed function directly from the CLI, but you can tail the CloudWatch logs by running sam logs -n hello --tail
and generate test events, such as an API Gateway event, by running sam local generate-event apigateway aws-proxy
- but perhaps the most useful one is sam sync
which first deploys your application and then listens to code changes in your Lambda functions and deploys them in a matter of seconds.
👷♂️ To-Don't example
Our To-Don't application's template.yaml
looks like this, with added comments explaining what's going on:
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
to-dont
Transform: AWS::Serverless-2016-10-31
# Default Lambda function config
Globals:
Function:
Timeout: 10
MemorySize: 512
Runtime: nodejs16.x
Resources:
persistFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/persist.handler
# Set an environment variable with the DynamoDB table name,
# based on the reference to the table defined below
Environment:
Variables:
TABLE_NAME: !Ref DynamoTable
Policies:
# Give Create/Read/Update/Delete Permissions to the DynamoDB table
- DynamoDBCrudPolicy:
TableName: !Ref DynamoTable
# Expose the function for POST requests on /item in an ApiGateway
Events:
Api:
Type: Api
Properties:
Path: /item
Method: POST
fanoutFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/fanout.handler
# Set an environment variable with the SNS topic ARN
Environment:
Variables:
TOPIC_ARN: !Ref SnsTopic
# Give the function permission to publish to the SNS topic
Policies:
- SNSPublishMessagePolicy:
TopicName: !GetAtt SnsTopic.TopicName
# Trigger the function when a new item is added to the DynamoDB
# table by listening to the DynamoDB stream
Events:
Stream:
Type: DynamoDB
Properties:
Stream: !GetAtt DynamoTable.StreamArn
StartingPosition: LATEST
BatchSize: 10
DynamoTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
SnsTopic:
Type: AWS::SNS::Topic
Outputs:
WebEndpoint:
Description: "API Gateway endpoint URL for Prod stage"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
You can find the full AWS SAM version of our To-Dont app here.
⚖️ Pros and Cons
Pros:
SAM Sync greatly speeds up the development flow.
It can be combined with AWS CDK.
Rapidly evolving
Cons:
Verbose config
No plugin system or reusable components
📚 Resources
Serverless Application Repository
SAM Serverless Patterns Collection
AWS CDK
AWS CDK, Cloud Development Kit, is very different from our previous two candidates. It takes an imperative approach and lets you write your infrastructure code in the same language that you already use to write the rest of the application, and then synthesizes the code to CloudFormation to deploy it. A CDK app consists of building blocks called constructs, and these constructs can be shared and re-used. A perhaps unexpected aspect is that the nature of how you build your CDK apps makes it very easy to write actual unit tests for your infrastructure.
It is, however, not strictly meant for building serverless applications, which means that the CLI does not include the kind of utility functionality around your development that other, more focused frameworks provide. But on the other hand, you can combine CDK with SAM to fill the gaps!
CDK is a fantastic tool for managing your AWS infrastructure, but it does lack some quality-of-life features and development utilities that are handy for serverless development.
🛠 Working with AWS CDK
You can install CDK globally by running npm install -g aws-cdk
.
When you initialize a CDK project by running cdk init
you'll be asked what language you want to use to define your infrastructure. A minimal project in TypeScript may look like this:
A /bin/my-app.ts
file that serves as the entry point of your application and creates the stack(s) that defines your application:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MyAppStack } from '../lib/my-app-stack';
const app = new cdk.App();
new MyAppStack(app, 'MyAppStack', {});
and then a lib/my-app-stack.ts
that defines what resources your app is built from:
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class MyAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_16_X,
code: lambda.Code.fromAsset('src'),
handler: 'hello.handler'
});
}
}
Given the above CDK app and configured AWS credentials, running cdk deploy
will deploy a CloudFormation stack with a ‘hello’ Lambda function.
When you change your infrastructure code, you can run cdk diff
to preview what changes would be made if it were deployed, and running cdk deploy --watch
will watch your files for changes and deploy them automatically.
A neat thing about the --watch
feature is that if the changes don't need a CloudFormation update, such as modifying Lambda function code, it'll skip the CloudFormation update and update the resource directly - greatly speeding up the workflow.
👷♂️ To-Don't example
The bin/to-dont.ts
file in our To-Don't project looks like this:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ToDontStack } from '../lib/to-dont-stack';
const app = new cdk.App();
new ToDontStack(app, 'CdkStack', {});
and the lib/to-dont-stack.ts
, a bit meatier, looks like this:
import { Construct } from 'constructs';
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as sns from 'aws-cdk-lib/aws-sns';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class ToDontStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create our DynamoDB table
const ddb = new dynamodb.Table(this, 'table', {
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'pk',
type: dynamodb.AttributeType.STRING,
},
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
// Create our SNS topic
const topic = new sns.Topic(this, 'topic');
// Create our API Gateway
const api = new apigateway.RestApi(this, 'api');
// Create the 'persist' Lambda function
const persistFunction = new NodejsFunction(this, 'persistFunc', {
// set the entry point to the function
entry: path.join(__dirname, '../src/functions/persist.ts'),
// set the DynamoDB table name as an environment variable
environment: {
TABLE_NAME: ddb.tableName,
},
});
// grant the persist function read/write permissions to the DynamoDB table
ddb.grantReadWriteData(persistFunction);
// Create the 'fanout' Lambda function
const fanoutFunction = new NodejsFunction(this, 'fanoutFunction', {
entry: path.join(__dirname, '../src/functions/fanout.ts'),
environment: {
TOPIC_ARN: topic.topicArn,
},
});
// grant the fanout function publish permissions to the SNS topic
topic.grantPublish(fanoutFunction);
// grant the fanout function Stream read permissions to the DynamoDB table
ddb.grantStreamRead(fanoutFunction);
// Add a DynamoDB stream event source to the fanout function
fanoutFunction.addEventSourceMapping('mapping', {
eventSourceArn: ddb.tableStreamArn,
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 10,
});
// Map the POST /item endpoint to the persist function
const itemResource = api.root.addResource('item');
itemResource.addMethod(
'POST',
new apigateway.LambdaIntegration(persistFunction, { proxy: true }),
);
}
}
You can find the full AWS CDK version of our To-Dont app here.
⚖️ Pros and Cons
Pros:
Use the same language to define your infrastructure as your application code (TypeScript, JavaScript, Python, Java, C# or Go)
Lets you create reusable constructs to share publicly or within your organization.
Easily write unit tests for your infrastructure.
Cons:
Every CDK app is going to look at least slightly different from any other CDK app, which makes it harder to understand a new project quickly.
Using high-level constructs that abstract away the nitty-gritty can accelerate your workflow, and it's easy to fall into the trap of abstracting away too much. Be careful; you're the one that owns the deployed infrastructure, and you need to understand what's actually getting deployed to do that responsibly.
📚 Resources
Getting started with the AWS CDK
Getting started with AWS SAM and the AWS CDK
CDK Serverless Patterns Collection
SST
SST is a batteries-included framework built on top of AWS CDK with a strong focus on developer experience. It includes high-level constructs for setting up common serverless resources, lets you build and deploy your frontend app in the same stack, and has a feature called Live Lambda that - hold on to your chai latte - lets you attach a debugger to a deployed Lambda function running in AWS.
SST does a great job at improving the developer experience and helps with the transition to deploying early and often as part of your development flow. And since SST uses CDK, you can use any CDK constructs in your applications, which means you benefit from the developments and community of that project too.
🛠 Working with SST
You can initialize a new SST application by running npm create sst@latest
which will let you choose from a few different starter examples.
A simple SST application could look like this:
A stacks/index.ts
that defines the entry point for our application, some default options, and which stack(s) are in it:
import { MyStack } from "./MyStack";
import { App } from "@serverless-stack/resources";
export default function (app: App) {
app.setDefaultFunctionProps({
runtime: "nodejs16.x",
srcPath: "services",
bundle: {
format: "esm",
},
});
app.stack(MyStack);
}
and then a Stack,stacks/MyAppStack.ts
, which includes the constructs that make up the application:
import { StackContext, Function } from "@serverless-stack/resources";
export function MyStack({ stack }: StackContext) {
new Function(stack, "helloFunction", {
handler: "functions/hello.handler",
});
}
Given the above configuration, npx sst deploy
will deploy a ‘hello’ Lambda function. Runningnpx sst start
similarly first deploys the application and starts to watch your files for changes and re-deploys the application when needed. If the changes don't require a CloudFormation update, like a Lambda function code update, the changes are instantly (no really, instantly) updated. While the debug session is active, your CloudWatch logs will be streamed directly to your CLI.
But perhaps the killer feature of this framework is that debug session allows you to attach an actual debugger to the process, which means you can use breakpoints and step through your code line-by-line - on a Lambda function running in AWS. Since the function, again, is actually running in AWS, you avoid a whole slew of problems that usually comes with trying to emulate Lambda, and other AWS services, locally.
👷♂️ To-Don't example
Thestacks/index.ts
in our To-Don't application looks like this:
import { ToDontStack } from "./ToDontStack";
import { App } from "@serverless-stack/resources";
export default function (app: App) {
// set default props for all functions
app.setDefaultFunctionProps({
runtime: "nodejs16.x",
srcPath: "services",
bundle: {
format: "esm",
},
});
// add our ToDontStack to the application
app.stack(ToDontStack);
}
and the stacks/ToDontStack.ts
looks like this:
import {
Api,
StackContext,
Table,
Topic
} from "@serverless-stack/resources";
export function ToDontStack({ stack }: StackContext) {
// create our SNS topic
const topic = new Topic(stack, "topic");
// create our DynamoDB table
const ddb = new Table(stack, "table", {
fields: {
pk: "string",
},
primaryIndex: { partitionKey: "pk" },
stream: "new_and_old_images",
consumers: {
fanout: {
function: {
handler: "functions/fanout.handler",
// bind the SNS Topic to the function so we can access it
// typesafely from the Lambda function instead of env vars
bind: [topic],
},
}
}
});
// create our ApiGateway
const api = new Api(stack, "api", {
routes: {
// map POST requests to /item to the persist function
"POST /item": {
// create the persist function
function: {
handler: "functions/persist.handler",
bind: [ddb],
},
},
},
});
// add a CloudFormation output for the API endpoint
stack.addOutputs({
ApiEndpoint: api.url,
});
}
You can find the full SST version of our To-Dont app here.
⚖️ Pros and Cons
Pros:
Live lambda debug really is a gamechanger
Useful abstractions for common serverless resources
Great development experience with fast deploys
Cons:
Every SST app is going to look, at least slightly, different from any other SST app, which makes it harder to understand a new project quickly
Using high-level constructs that abstract away the nitty-gritty can accelerate your workflow, and it's easy to fall into the trap of abstracting away too much. Be careful; you're the one that owns the deployed infrastructure, and you need to understand what's actually getting deployed to do that responsibly.
📚 Resources
CDK Serverless Patterns Collection
🥈Honorable mentions - niche & generic options
There are a couple of fairly common options for building serverless AWS apps that I've chosen not to include in this list, either because they're more generic and aren't focused on AWS or because they are more niche and focus on a subset of services or developers. These tools are all excellent at what they do though, so here are a few honorable mentions that didn't make the list, but may still be the best choice for you, depending on your type of application, your organization's policies and standards, or your preferences:
Terraform
Terraform is a widely used Infrastructure-as-Code tool with open-source modules available for virtually any API, including AWS. In contrast to all previously discussed tools, Terraform does not produce and deploy CloudFormation. Instead, it talks directly to the AWS API and keeps track of its state on its own.
Pulumi
Pulumi is another universal Infrastructure-as-Code tool, but instead of using a DSL, you can use Python, JavaScript, YAML, or other familiar programming- and markup languages. Like Terraform, Pulumi manages its own state instead of using CloudFormation.
Architect
Architect is a heavily opinionated framework for building FWA's, Functional Web Apps. It uses AWS SAM under the hood but provides a layer on top with simplified abstractions that lets developers define and use AWS infrastructure without necessarily knowing what service is backing their "events" construct.
AWS Amplify
Amplify is a tool meant to let frontend- and mobile developers build full-stack apps on AWS. It, among other things, features a CLI and a web console that helps you build out your backend and takes care of hosting and deployment. It's an excellent tool for rapid prototyping and building mobile apps.
☑️ Summary
In conclusion, there are a bunch of different serverless frameworks available to developers today, each with its own set of features and capabilities. In this article, we looked at some of the most popular frameworks and demonstrated how they could be used to build serverless applications. We also compared their features and discussed their pros and cons.
It's worth noting that the serverless space is rapidly evolving and changing. New features and capabilities are being added all the time, and new frameworks are constantly emerging.
CDK has been disrupting the space in the last years, but Infrastructure from Code is another interesting new concept with tools such as Ampt (previously Serverless Cloud) reimagining how we think about infrastructure (if you're interested in this concept, Allen Helton has a great post on it here).
I'm excited to see what the future holds for serverless and the tooling around it. Next year's version of this blog post is sure to be even more exciting as the space continues to grow and evolve. Keep an eye out for new developments, and don't be afraid to experiment with different frameworks to see which one works best for your particular use case!
Hi there, I'm Sebastian Bille! If you enjoyed this post or just want a constant feed of memes, AWS/serverless talk, and the occasional new blog post, make sure to follow me on Twitter at @TastefulElk or on LinkedIn 👋
Elva is a serverless-first consulting company that can help you transform or begin your AWS journey for the future