Migrating From Serverless Framework

In this guide we'll look at how to migrate a Serverless Framework app to SST.

Note that, this document is a work in progress. If you have experience migrating your Serverless Framework app to SST, please consider contributing.

Incrementally Adopting SST#

SST has been designed to be incrementally adopted. This means that you can continue using your existing Serverless Framework app while slowly moving over resources to SST. By starting small and incrementally adding more resources, you can avoid a wholesale rewrite.

Let's assume you have an existing Serverless Framework app. To get started, we'll first set up a new SST project in the same directory.

A hybrid Serverless Framework and SST app#

To make it an easier transition, we'll start by merging your existing Serverless Framework app with a newly created SST app.

Your existing app can either have one service or be a monorepo with multiple services.

  1. In a temporary location, run npx create-serverless-stack@latest my-sst-app or use the --language typescript option if your project is in TypeScript.
  2. Copy the sst.json file and the src/ and lib/ directories.
  3. Copy the scripts, dependencies, and devDependencies from the package.json file in the new SST project root.
  4. Copy the .gitignore file and append it to your existing .gitignore file.
  5. If you are using TypeScript, you can also copy the tsconfig.json.
  6. Run npm install.

Now your directory structure should look something like this. The src/ directory is where all the Lambda functions in your Serverless Framework app are placed.

serverless-app
โ”œโ”€โ”€ node_modules
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ serverless.yml
โ”œโ”€โ”€ sst.json
โ”œโ”€โ”€ lib
| โ”œโ”€โ”€ MyStack.js
| โ””โ”€โ”€ index.js
โ””โ”€โ”€ src
โ”œโ”€โ”€ lambda1.js
โ””โ”€โ”€ lambda2.js

And from your project root you can run both the Serverless Framework and SST commands.

This also allows you to easily create functions in your new SST app by pointing to the handlers in your existing app.

Say you have a Lambda function defined in your serverless.yml.

serverless.yml
functions:
hello:
handler: src/lambda1.main

You can now create a function in your SST app using the same source.

SST
new sst.Function(this, "MySnsLambda", {
handler: "src/lambda1.main",
});

Monorepo with multiple Serverless Framework services#

If you have a multple Serverless Framework services in the same repo, you can still follow the steps above to create a single SST app. This is because you can define multiple stacks in the same SST app. Where as each Serverless Framework service can only contain a single stack.

After the SST app is created, your directory structure should look something like this.

serverless-app
โ”œโ”€โ”€ node_modules
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ sst.json
โ”œโ”€โ”€ lib
| โ”œโ”€โ”€ MyStack.js
| โ””โ”€โ”€ index.js
โ””โ”€โ”€ services
โ”œโ”€โ”€ serviceA
| โ”œโ”€โ”€ serverless.yml
| โ”œโ”€โ”€ lambda1.js
| โ””โ”€โ”€ lambda2.js
โ””โ”€โ”€ serviceB
โ”œโ”€โ”€ serverless.yml
โ”œโ”€โ”€ lambda3.js
โ””โ”€โ”€ lambda4.js

The src/ directory is where all the Lambda functions in your Serverless Framework app are placed.

Add new services to SST#

Next, if you need to add a new service or resource to your Serverless Framework app, you can instead do it directly in SST.

For example, say you want to add a new SQS queue resource.

  1. Start by creating a new stack in the lib/ directory. Something like, lib/MyNewQueueService.js.
  2. Add the new stack to the list in lib/index.js.

Reference stack outputs#

Now that you have two separate apps side-by-side, you might find yourself needing to reference stack outputs between each other.

Reference a Serverless Framework stack output in SST#

To reference a Serverless Framework stack output in SST, you can use the cdk.Fn.import_value function.

For example:

// This imports an S3 bucket ARN and sets it as an environment variable for
// all the Lambda functions in the new API.
new sst.Api(this, "MyApi", {
defaultFunctionProps:
environment: {
myKey: cdk.Fn.import_value("exported_key_in_serverless_framework")
}
},
routes: {
"GET /notes" : "src/list.main",
"POST /notes" : "src/create.main",
"GET /notes/{id}" : "src/get.main",
"PUT /notes/{id}" : "src/update.main",
"DELETE /notes/{id}" : "src/delete.main",
}
});

Reference SST stack outputs in Serverless Framework#

You might also want to reference a newly created resource in SST in Serverless Framework.

SST
// Export in an SST stack
new CfnOutput(this, "TableName", {
value: bucket.bucketArn,
exportName: "MyBucketArn",
});
Serverless Framework
// Importing in serverless.yml
!ImportValue MyBucketArn

Referencing SST stack outputs in other SST stacks#

And finally, to reference stack outputs across stacks in your SST app.

StackA.js
this.bucket = new s3.Bucket(this, "MyBucket");
StackB.js
// stackA's bucket is passed to stackB
const { bucket } = this.props;
// SST will implicitly set the exports in stackA
// and imports in stackB
bucket.bucketArn;

Reference Serverless Framework resources#

The next step would be to use the resources that are created in your Serverless Framework app. You can reference them directly in your SST app, so you don't have to recreate them.

For example, if you've already created an SNS topic in your Serverless Framework app, and you want to add a new function to subscribe to it:

import { Topic } from "@aws-cdk/aws-sns";
// Lookup the existing SNS topic
const snsTopic = Topic.fromTopicArn(
this,
"ImportTopic",
"arn:aws:sns:us-east-2:444455556666:MyTopic"
);
// Add 2 new subscribers
new sst.Topic(this, "MyTopic", {
snsTopic,
subscribers: ["src/subscriber1.main", "src/subscriber2.main"],
});

Migrate existing services to SST#

There are a couple of strategies if you want to migrate your Serverless Framework resources to your SST app.

Proxying#

This applies to API endpoints and it allows you to incrementally migrate API endpoints to SST.

note

Support for this strategy hasn't been implemented in SST yet.

Suppose you have a couple of routes in your serverless.yml.

functions:
usersList:
handler: src/usersList.main
events:
- httpApi:
method: GET
path: /users
usersGet:
handler: src/usersGet.main
events:
- httpApi:
method: GET
path: /users/{userId}

And you are ready to migrate the /users endpoint but don't want to touch the other endpoints yet.

You can add the route you want to migrate, and set a catch all route to proxy requests the rest to the old API.

const api = new sst.Api(this, "Api", {
routes: {
"GET /users": "src/usersList.main",
// "$default" : proxy to old api,
},
});

Now you can use the new API endpoint in your frontend application. And remove the old route from the Serverless Framework app.

Resource swapping#

This is suitable for migrating resources that don't have persistent data. So, SNS topics, SQS queues, and the like.

Imagine you have an existing SNS topic named MyTopic.

  1. Create a new topic in SST called MyTopic.sst and add a subscriber with the same function code.

  2. Now in your app, start publishing to the MyTopic.sst instead of MyTopic.

  3. Remove the old MyTopic resource from the Serverless Framework app.

Optionally, you can now create another new topic in SST called MyTopic and follow the steps above to remove the temporary MyTopic.sst topic.

Migrate only the functions#

Now for resources that have persistent data like DynamoDB and S3, it won't be possible to remove them and recreate them. For these cases you can leave them as-is, while migrating over the DynamoDB stream subscribers and S3 bucket event subscribers as a first step.

Here's an example for DynamoDB streams. Assume you have a DynamoDB table that is named based on the stage it's deployed to.

serverless.yml
resources:
Resources:
MyTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.stage}-MyTable
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: noteId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: noteId
KeyType: RANGE
BillingMode: 'PAY_PER_REQUEST'
StreamSpecification:
StreamViewType: NEW_IMAGE

Now in SST, you can import the table and create an SST function to subscribe to its streams.

// Import table
const table = dynamodb.fromTableName(
this,
"MyTable",
`${this.node.root.stage}-MyTable`
);
// Create a Lambda function
const processor = new sst.Function(this, "Processor", "processor.main");
// Subscribe function to the streams
processor.addEventSource(
new DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
})
);

Workflow#

A lot of the commands that you are used to using in Serverless Framework translate well to SST.

Serverless FrameworkSST
serverless invoke localsst start
serverless packagesst build
serverless deploysst deploy
serverless removesst remove

SST also supports the IS_LOCAL environment variable that gets set in your Lambda functions when run locally.

Invoking locally#

With the Serverless Framework you need to run the following command serverless invoke local -f function_name to invoke a function locally.

With SST this can be done via PostMan, Hopscotch, curl or any other API client. However, with this event you are actually sending a request to API Gateway which then invokes your Lambda.

CI/CD#

If you are using GitHub Actions, Circle CI, etc., to deploy Serverless Framework apps, you can now add the SST versions to your build scripts.

# Deploy the defaults
npx sst deploy
# To a specific stage
npx sst deploy --stage prod
# To a specific stage and region
npx sst deploy --stage prod --region us-west-1
# With a different AWS profile
AWS_PROFILE=production npx sst deploy --stage prod --region us-west-1

Serverless Dashboard#

If you are using the Serverless Dashboard, you can try out Seed instead. It supports Serverless Framework and SST. So you can deploy the hybrid app that we've created here.

Seed has a fully-managed CI/CD pipeline, monitoring, real-time alerts, and deploys a lot faster thanks to the Incremental Deploys. It also gives you a great birds eye view of all your environments.

Lambda Function Triggers#

Following is a list of all the Lambda function triggers available in Serverless Framework. And the support status in SST (or CDK).

TypeStatus
HTTP APIAvailable
API Gateway REST APIAvailable
WebSocket APIAvailable
ScheduleAvailable
SNSAvailable
SQSAvailable
DynamoDBAvailable
KinesisAvailable
S3Available
CloudWatch EventsAvailable
CloudWatch LogsAvailable
EventBus Event.Available
EventBridge EventAvailable
Cognito User PoolAvailable
ALBAvailable
Alexa SkillAvailable
Alexa Smart HomeAvailable
IoTAvailable
CloudFrontComing soon
IoT Fleet ProvisioningComing soon
KafkaComing soon
MSKComing soon

Plugins#

Serverless Framework supports a long list of popular plugins. In this section we'll look at how to adopt their functionality to SST.

To start with, let's look at the very popular serverless-offline plugin. It's used to emulate a Lambda function locally but it's fairly limited in the workflows it supports. There are also a number of other plugins that work with serverless-offline to support various other Lambda triggers.

Thanks to sst start, you don't need to worry about using them anymore.

PluginAlternative
serverless-offlinesst start
serverless-offline-snssst start
serverless-offline-ssmsst start
serverless-dynamodb-localsst start
serverless-offline-schedulersst start
serverless-step-functions-offlinesst start
serverless-offline-direct-lambdasst start
CoorpAcademy/serverless-pluginssst start
serverless-plugin-offline-dynamodb-streamsst start

Let's look at the other popular Serverless Framework plugins and how to set them up in SST.

PluginStatus
serverless-webpackSST uses esbuild to automatically bundle your functions
serverless-domain-managersst.Api supports custom domains
serverless-pseudo-parametersCloudFormation pseudo parameters are not necessary in CDK
serverless-step-functionsAvailable in CDK
serverless-plugin-aws-alertsAvailable in CDK
serverless-plugin-typescriptSST natively supports TypeScript
serverless-apigw-binaryAvailable in CDK
serverless-plugin-tracingSupported by SST
serverless-aws-documentationComing soon
serverless-dotenv-pluginComing soon
serverless-plugin-split-stacksComing soon
serverless-plugin-include-dependenciesComing soon
serverless-iam-roles-per-functionSupported by SST
serverless-plugin-monorepoSST supports monorepo setups automatically
serverless-log-forwardingAvailable in CDK
serverless-plugin-lambda-dead-letterAvailable in CDK
serverless-plugin-stage-variablesAvailable in CDK
serverless-stack-outputSupported by SST
serverless-plugin-scriptsComing soon
serverless-finchAvailable in CDK
serverless-stage-managerSupported by SST
serverless-plugin-log-subscriptionAvailable in CDK
serverless-plugin-git-variablesAvailable in CDK
serverless-dynamodb-autoscalingAvailable in CDK
serverless-aws-aliasAvailable in CDK
serverless-s3-removerComing soon
serverless-s3-syncComing soon
serverless-appsync-pluginAvailable in CDK
serverless-scriptable-pluginComing soon
serverless-mysqlComing soon
serverless-plugin-canary-deploymentsComing soon
serverless-prune-pluginComing soon

Examples#

A list of examples showing how to use Serverless Framework triggers or plugins in SST.

Triggers#

HTTP API#

serverless.yml
functions:
listUsers:
handler: listUsers.main
events:
- httpApi:
method: GET
path: /users
createUser:
handler: createUser.main
events:
- httpApi:
method: POST
path: /users
getUser:
handler: getUser.main
events:
- httpApi:
method: GET
path: /users/{id}
SST
new Api(this, "Api", {
routes: {
"GET /users": "listUsers.main",
"POST /users": "createUser.main",
"GET /users/{id}": "getUser.main",
},
});

API Gateway REST API#

serverless.yml
functions:
listUsers:
handler: listUsers.main
events:
- http:
method: GET
path: /users
createUser:
handler: createUser.main
events:
- http:
method: POST
path: /users
getUser:
handler: getUser.main
events:
- http:
method: GET
path: /users/{id}
SST
new ApiGatewayV1Api(this, "Api", {
routes: {
"GET /users": "listUsers.main",
"POST /users": "createUser.main",
"GET /users/{id}": "getUser.main",
},
});

WebSocket#

serverless.yml
functions:
connectHandler:
handler: connect.main
events:
- websocket: $connect
disconnectHandler:
handler: disconnect.main
events:
- websocket:
route: $disconnect
defaultHandler:
handler: default.main
events:
- websocket:
route: $default
sendMessageHandler:
handler: sendMessage.main
events:
- websocket:
route: sendMessage
SST
new WebSocketApi(this, "Api", {
routes: {
$connect: "src/connect.main",
$default: "src/default.main",
$disconnect: "src/disconnect.main",
sendMessage: "src/sendMessage.main",
},
});

Schedule#

serverless.yml
functions:
crawl:
handler: crawl.main
events:
- schedule: rate(2 hours)
SST
new Cron(this, "Crawl", {
schedule: "rate(2 hours)",
job: "crawl.main",
});

SNS#

serverless.yml
functions:
subscriber:
handler: subscriber.main
events:
- sns: dispatch
subscriber2:
handler: subscriber2.main
events:
- sns: dispatch
SST
new Topic(this, "Dispatch", {
subscribers: ["subscriber.main", "subscriber2.main"],
});

SQS#

serverless.yml
functions:
consumer:
handler: consumer.main
events:
- sqs:
arn:
Fn::GetAtt:
- MyQueue
- Arn
resources:
Resources:
MyQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: ${self:custom.stage}-MyQueue
SST
new Queue(this, "MyQueue", {
consumer: "consumer.main",
});

DynamoDB#

serverless.yml
functions:
processor:
handler: processor.main
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- MyTable
- StreamArn
resources:
Resources:
MyTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.stage}-MyTable
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: noteId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: noteId
KeyType: RANGE
BillingMode: 'PAY_PER_REQUEST'
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
SST
new Table(this, "MyTable", {
fields: {
userId: TableFieldType.STRING,
noteId: TableFieldType.STRING,
},
primaryIndex: { partitionKey: "noteId", sortKey: "userId" },
stream: true,
consumers: ["processor.main"],
});

Kinesis#

serverless.yml
functions:
processor:
handler: processor.main
events:
- stream:
type: kinesis
arn:
Fn::Join:
- ":"
- - arn
- aws
- kinesis
- Ref: AWS::Region
- Ref: AWS::AccountId
- stream/MyKinesisStream
SST
// Create stream
const stream = new kinesis.Stream(this, "MyStream");
// Create Lambda function
const processor = new sst.Function(this, "Processor", "processor.main");
// Subscribe function to streams
processor.addEventSource(
new KinesisEventSource(stream, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
})
);

S3#

serverless.yml
functions:
processor:
handler: processor.main
events:
- s3:
bucket: MyBucket
event: s3:ObjectCreated:*
rules:
- prefix: uploads/
SST
// Create bucket
const bucket = new s3.Bucket(this, "MyBucket");
// Create Lambda function
const processor = new sst.Function(this, "Processor", "processor.main");
// Subscribe function to streams
processor.addEventSource(
new S3EventSource(bucket, {
events: [s3.EventType.OBJECT_CREATED],
filters: [{ prefix: "uploads/" }],
})
);

CloudWatch Events#

serverless.yml
functions:
myCloudWatch:
handler: myCloudWatch.handler
events:
- cloudwatchEvent:
event:
source:
- "aws.ec2"
detail-type:
- "EC2 Instance State-change Notification"
detail:
state:
- pending
SST
const processor = new sst.Function(this, "Processor", "processor.main");
const rule = new events.Rule(this, "Rule", {
eventPattern: {
source: ["aws.ec2"],
detailType: ["EC2 Instance State-change Notification"],
},
});
rule.addTarget(new targets.LambdaFunction(processor));

CloudWatch Logs#

serverless.yml
functions:
processor:
handler: processor.main
events:
- cloudwatchLog:
logGroup: "/aws/lambda/hello"
filter: "{$.error = true}"
SST
const processor = new sst.Function(this, "Processor", "processor.main");
new SubscriptionFilter(this, "Subscription", {
logGroup,
destination: new LogsDestinations.LambdaDestination(processor),
filterPattern: FilterPattern.booleanValue("$.error", true),
});

EventBus Event#

serverless.yml
functions:
myFunction:
handler: processor.main
events:
- eventBridge:
eventBus:
Fn::GetAtt:
- MyEventBus
- Arn
pattern:
source:
- acme.transactions.xyz
resources:
Resources:
MyEventBus:
Type: AWS::Events::EventBus
Properties:
Name: MyEventBus
SST
const processor = new sst.Function(this, "Processor", "processor.main");
const rule = new events.Rule(this, "MyEventRule", {
eventBus: new events.EventBus(this, "MyEventBus"),
eventPattern: {
source: ["acme.transactions.xyz"],
},
});
rule.addTarget(new targets.LambdaFunction(processor));

EventBridge Event#

serverless.yml
functions:
myFunction:
handler: processor.main
events:
- eventBridge:
pattern:
source:
- aws.cloudformation
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- cloudformation.amazonaws.com
SST
const processor = new sst.Function(this, "Processor", "processor.main");
const rule = new events.Rule(this, "rule", {
eventPattern: {
source: ["aws.cloudformation"],
detailType: ["AWS API Call via CloudTrail"],
detail: {
eventSource: ["cloudformation.amazonaws.com"],
},
},
});
rule.addTarget(new targets.LambdaFunction(processor));

Cognito User Pool#

serverless.yml
functions:
preSignUp:
handler: preSignUp.main
events:
- cognitoUserPool:
pool: MyUserPool
trigger: PreSignUp
existing: true
SST
new sst.Auth(this, "Auth", {
cognito: {
triggers: {
preSignUp: "src/preSignUp.main",
},
},
});

Plugins#

serverless-domain-manager#

serverless.yml
plugins:
- serverless-domain-manager
custom:
customDomain:
domainName: api.domain.com
function:
listUsers:
handler: src/listUsers.main
events:
- httpApi:
method: GET
path: /users
SST
new Api(this, "Api", {
customDomain: "api.domain.com",
routes: {
"GET /users": "src/listUsers.main",
},
});

serverless-pseudo-parameters#

serverless.yml
plugins:
- serverless-pseudo-parameters
resources:
Resources:
S3Bucket:
Type: AWS::S3::Bucket,
DeleteionPolicy: Retain
Properties:
BucketName: photos-#{AWS::AccountId}
SST
new s3.Bucket(this, "S3Bucket", {
bucketName: `photos-${stack.account}`
};

serverless-step-functions#

serverless.yml
plugins:
- serverless-step-functions
functions:
hello:
handler: hello.main
StartAt: Wait
States:
Wait:
Type: Wait
Seconds: 300
Next: Hello
Hello:
Type: Task
Resource:
Fn::GetAtt:
- hello
- Arn
Next: Decide
Decide:
Type: Choice
Choices:
- Variable: $.status
StringEquals: Approved
Next: Success
Default: Failed
Success:
Type: Succeed
Failed:
Type: Fail
SST
// Define each state
const sWait = new sfn.Wait(this, "Wait", {
time: sfn.WaitTime.duration(300),
});
const sHello = new tasks.LambdaInvoke(this, "Hello", {
lambdaFunction: new sst.Function(this, "Hello", "hello.main"),
});
const sFailed = new sfn.Fail(this, "Failed");
const sSuccess = new sfn.Succeed(this, "Success");
// Define state machine
new sfn.StateMachine(this, "StateMachine", {
definition: sWait
.next(sHello)
.next(
new sfn.Choice(this, "Job Approved?")
.when(sfn.Condition.stringEquals("$.status", "Approved"), sSuccess)
.otherwise(sFailed)
),
});

serverless-plugin-aws-alerts#

serverless.yml
plugins:
- serverless-plugin-aws-alerts
custom:
alerts:
stages:
- production
topics:
alarm:
topic: ${self:service}-${opt:stage}-alerts-alarm
notifications:
- protocol: email
endpoint: foo@bar.com
alarms:
- functionErrors
SST
// Send an email when a message is received
const topic = new sns.Topic(stack, "AlarmTopic");
topic.addSubscription(new subscriptions.EmailSubscription("foo@bar.com"));
// Post a message to topic when an alarm breaches
new cloudwatch.Alarm(this, "Alarm", {
metric: lambda.metricAllErrors(),
threshold: 100,
evaluationPeriods: 2,
});
alarm.addAlarmAction(new cloudwatchActions.SnsAction(topic));

serverless-stage-manager#

serverless.yml
plugins:
- serverless-stage-manager
custom:
stages:
- dev
- staging
- prod
SST
if (!["dev", "staging", "prod"].includes(app.stage)) {
throw new Error("Invalid stage");
}