Lambdas with Serverless Framework

Creating APIs with AWS Lambdas and Serverless Framework

Published: 17 October 2020

serverless NodeJS APIs AWS Lambda 

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:

  • If we didn't provide region and stage in serverless file (under provider), we can add them or override them directly when invoking the deploy command.
  • Default values for region and stage are us-east-1 and dev respectively
  • The deploy command will create a zip package and upload a copy to S3 before uploading it to respective functions
  • Each function on AWS will have the complete project stored in it (not just the function's own code)
  • Each time a function is deployed, a version is added to the function name to differentiate it from previous deployment (eg - firstLamda-1 or firstLamda-2)
  • The whole function name in AWS console will be:
serviceName-stage-functionName-version
OR
my-first-app-dev-firstLambda-1
  • All functions need to be deployed first time, but after that they can be updated individually by running:
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

Serverless User Guide Intro

Serverless AWS CLI Reference

Lambda Retries Attempts

Serverless Deployment

Error Handling in Lambda

Serverless Example Repos