⚡️Serverless Frameworks for 2023

⚡️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

Getting started with AWS SAM

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 diffto 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

SST Quick Start

Live Lambda Development

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 👋

Did you find this article valuable?

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