Lambdas with Serverless Framework
Creating APIs with AWS Lambdas and Serverless Framework
Published: 17 October 2020
Installation and Setup
Serverless Framework can be installed globally with:
npm install -g serverless
This will allow us to run commands for locally invoking and testing our lambdas as well as deploying them to AWS.
We also need a local AWS Profile setup so serverless framework can assume the role needed for deploying and accessing other AWS services on our behalf. Install AWS-cli first and then run the following from our terminal:
aws configure
// provide your accessKeyId, secretAccessKey & region to complete the setup
This will create a default AWS profile which will be later used by serverless.
Basics
Assuming we have a NodeJS project where we would like to create APIs with AWS Lambda, we will need to create a file named:
serverless.yml
OR
serverless.json
OR
serverless.js
This will be picked by serverless framework whenever we will execute any serverless or sls
commands at our project's root. This file contains configuration for our lambdas and underlying resources.
A project can have multiple of these files depending on how we split the project. Usually, its best practice to break them up with different services. Let's assume we have a service called my-first-app
:
//serverless.js
module.exports = {
frameworkVersion: "2.12.0",
service: "my-first-app",
provider: {
name: "aws",
runtime: "nodejs12.x",
region: "ap-southeast-2",
profile: "default",
},
}
frameworkVersion
--> is your current serverless framework version (serverless --version)
services
--> name of your service
provider
--> contains some basic setup configuration (including profile, in case you want to use a different AWS profile other than default)
Functions
Let's say we have a simple Lambda function setup in Lambdas/firstLambda.js
file at the project root:
//Lambdas/firstLambda.js
module.exports.myLambda = async (event, context) => {
return {
body: "i am first function for " + event.name,
statusCode: 200,
}
}
Now, lets declare this inside serverless.js
file:
module.exports = {
frameworkVersion: "2.12.0",
service: "my-first-app",
provider: {
name: "aws",
runtime: "nodejs12.x",
region: "ap-southeast-2",
profile: "default",
stage: "dev"
},
// starting functions object which can contain multiple functions
functions: {
firstLambda: {
handler: "Lambdas/firstLambda.myLambda",
event: [{ http: "get /first" }],
},
},
}
// could also write event prop as:
events: [
{
http: { path: "/first", method: "GET", cors: true }
}
],
Functions
is an object which can contain info on other functions. In the above case, we have declared a function named firstLambda
with following props:
handler
--> where the handler code lives for this function
event
--> kind of event that can trigger this Lambda (we have chosen http endpoint which translates to API Gateway endpoint /first
and only accepts GET
requests). We can add as many events as we want.
Local Invocation
To invoke this locally and to make sure it gives back the correct result, we can run:
sls invoke local --function <function-name>
sls invoke local --function firstLambda
The function will be invoked however, no event
argument will be passed to our Lambda since we didn't provide a payload. To do so, we must provide --path
flag and include the path to the json file which contains our payload.
Lets assume our firstLambdaPayload.json
file lives at the root of the project, we can use it like so:
sls invoke local --function <function-name> --path <path-to-json-file>
sls invoke local --function firstLambda --path ./firstLambdaPayload.json
NOTE: If our Lambda function is using the return
keyword to return response back, we must add async
keyword to our function otherwise it will throw an internal server error due to Proxy setup.
To add environment variables which will be accessible inside Lambda function with process.env
, we can add -e
flag like so:
sls invoke local --function firstLambda --path ./firstLambdaPayload.json -e AUTHOR_NAME=homersimpson
OR we can add them to provider
object inside serverless
file which will apply them to all functions:
provider:{
name: "aws",
environment:{
SYSTEM_NAME: "mySystem"
TABLE_NAME: "tableName1"
}
}
OR can supply to individual functions as well:
functions: {
firstLambda: {
handler: "Lambdas/firstLambda.myLambda",
event: [{ http: "get /first" }],
},
environment:{
AUTHOR_NAME: "Homer Simpson"
}
},
Deployment
To deploy our function, we can run:
serverless deploy
OR
serverless deploy --region ap-southeast-2 --stage beta
This will deploy all functions (if we had more than one) via cloudformation stack to AWS.
Some things to notice:
region
and stage
in serverless
file (under provider), we can add them or override them directly when invoking the deploy command.region
and stage
are us-east-1
and dev
respectivelyserviceName-stage-functionName-version
OR
my-first-app-dev-firstLambda-1
serverless deploy function --function firstLambda
(this is faster since it doesn't go through all cloudformation stack updates).
IAM Roles
To allow access to other AWS resources, our functions can be given IAM roles via the provider like so:
provider:{
name: "aws",
runtime: "nodejs12.x",
iamRoleStatement:[
{
Effect: "Allow",
Action:[
"dynamodb:DescribeTable",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:PutItem",
"dynamodb:GetItem"
]
},
// add more roles as needed
]
}
We can also attach managed polcies in the provider via the iamManagedPolicies
prop.
Error/Success Handling
Lambda can throw 2 types of errors:
Invocation Errors
- function couldn't be started (due to bad request/format)
throws 4xx, 5xx errors
Function Runtime Errors
- when something goes wrong in the code (we handle this ourselves)
always gives 200 OK
For invocation errors, Lambda retries the function a certain number of times before it stops. We can set this with maximumRetryAttempts
prop under the function declaration in serverless
file (value can be between 0 and 2).
functions: {
firstLambda: {
handler: "Lambdas/firstLambda.myLambda",
event: [{ http: "get /first" }],
maximumRetryAttempts: 1
},
},
We can also use the onError
prop to send the failed response to a Dead Letter Queue (DLQ)
which is an SNS topic. We can then hook up other Lambda functions to it to handle errors in a meaningful way (like sending messages to a specific slack channel).
functions: {
firstLambda: {
handler: "Lambdas/firstLambda.myLambda",
event: [{ http: "get /first" }],
onError: "arn-of-sns-topic"
},
},
NOTE: For Runtime errors, we must always be catching those ourselves. Alternatively, Lambda sends a X-Amz-Function-Error
header alongwith the JSON response and we can use it to do other things as needed.
By using destinations
prop, we can invoke other Lambdas directly on either success
or failure
of our own Lambda function like so:
functions: {
firstLambda: {
handler: "Lambdas/firstLambda.myLambda",
event: [{ http: "get /first" }],
destinations:{
onSuccess: "arn-of-other-lambda-function",
onFailure: "arn-of-some-other-lambda-function"
}
},
},
Deleting serverless APIs
To delete the whole stack created by serverless, we can run:
serverless remove
serverless remove --stage dev
serverless remove --stage dev --region ap-southeast-2