Serverless: Creating light and lean function packages

Its a best practice throughout all Serverless Platforms to keep the function packages small, lightweight and lean in order to improve performance and cold start times. A general rule of thumb is to only include those dependencies in a package which are absolutely necessary and getting rid of all redundant items.

The Serverless Framework allows us to manage our Cloud Functions on AWS and Azure. However, by default, the framework packages everything together for all the functions which results in redundant dependencies being packaged together and increasing the package sizes. This can be fixed with just a few configurations!

Setting up the base project!

serverless

The CLI should set up the basic structure. I’ll be using Node.js and AWS for this post but feel free to change the provider and the runtime. Replace the contents of serverless.yml:

service: serveless-medium

provider:
name: aws
runtime: nodejs12.x
region: ap-southeast-1
role: arn:aws:iam::XXXXXXXXXXXX:role/lambda-role

For this example, we wont use the default handler but create 2 of our own. randomGenerator.js and uuidGenerator.js:

//randomGenerator.js'use strict';
const Chance = require('chance');
module.exports.handler = async event => {
const chance = new Chance();
return {
statusCode: 200,
body: chance.string()
};
};
//uuidGenerator.js'use strict';
const uuid = require('uuid');
module.exports.handler = async event => {
const generatedId = uuid.v4();
return {
statusCode: 200,
body: generatedId
};
};

Lets add these function definitions at the end of our serverless.yml:

functions:
randomGenerator:
handler: randomGenerator.handler
uuidGenerator:
handler: uuidGenerator.handler

Since we’re using some dependencies in our functions (chance.js in randomGenerator.js and uuid.js in uuidGenerator.js), we need to add these dependencies to our project as well. So create a package.json and do npm install:

//package.json{
"dependencies": {
"chance": "^1.1.6",
"uuid": "^3.3.3"
}
}

The project structure should look like this:

- node_modules
- .gitignore
- package.json
- package-lock.json
- randomGenerator.js
- serverless.yml
- uuidGenerator.js

Run serverless deploy and we should be able to see our lambda functions created. This is what should be created:

uuid generator function
random generator function

Whats wrong here is that even those dependencies / handler functions which are unrelated have been packaged together! the chance.js package and randomGenerator.js should not be packaged with the uuidGenerator function and vice versa. This bloating will increase even more as our functions and their dependencies increase.

Our aim is to separate out the dependencies of each function and only package the required dependencies.

Separating the functions and their dependencies!

  1. Create a folder “functions”.
  2. Create 2 sub folders “uuidGenerator” and “randomGenerator”.
  3. Move randomGenerator.js inside the randomGenerator sub folder and move uuidGenerator.js inside the uuidGenerator sub folder.
  4. Break the package.json file into two and add to the respective folders:
// functions/randomGenerator/package.json{
"dependencies": {
"chance": "^1.1.6"
}
}
// functions/uuidGenerator/package.json
{
"dependencies": {
"uuid": "^3.3.3"
}
}

5. Run npm install inside each subfolder to install the dependencies.

6. Delete node_modules from the root directory.

Our project structure should look like this now:

- functions
- randomGenerator
- node_modules
- package.json
- package-lock.json
- randomGenerator.js
- uuidGenerator
- node_modules
- package.json
- package-lock.json
- uuidGenerator.js
- .gitignore
- serverless.yml

Upon looking at the node_modules directory in each of the function folders, we can see that only those packages have been added which are relevant to that function.

Updating serverless.yml

functions:
randomGenerator:
handler: functions/randomGenerator/randomGenerator.handler
package:
individually: true
exclude:
- "./**"
include:
- ./functions/randomGenerator/randomGenerator.js
- ./functions/randomGenerator/node_modules/**
- ./functions/randomGenerator/package.json
uuidGenerator:
handler: functions/uuidGenerator/uuidGenerator.handler
package:
individually: true
exclude:
- "./**"
include:
- ./functions/uuidGenerator/uuidGenerator.js
- ./functions/uuidGenerator/node_modules/**
- ./functions/uuidGenerator/package.json
  1. Since our handler functions now exist inside a folder, we need to update the path. For example, in case of the randomGenerator function, the handler now becomes: functions/randomGenerator/randomGenerator.handler.
  2. We have added a new property called package which allows us to have more control over the packaging of our functions.
  3. individually: true tells the framework to package the functions individually.
  4. Exclude tells the framework to not package anything by default with the function. We might have entire test suites and other dev dependencies which are not needed in the package. With this, we can exclude all such items.
  5. With Include, we individually and explicitly specify the files and folders that we want to package with a specific function. For example, in case of the randomGenerator function, we only want to package the handler file, the node_modules and the package.json.

run serverless deploy and our lambda functions should be updated like this:

only the selected items have been packaged for randomGenerator function
only the selected items have been packaged for uuidGenerator function

We can see that as opposed to before, only the items that we selected have been packaged thereby making our package size as minimal as possible. Go ahead and try invoking the lambda functions with test events.

And that’s it!

In the next part, We’ll see how we can break down the serverless.yml file into multiple chunks for better management!

PS: The Source Code for this part has been uploaded on Github for reference.

This article is a part of my 5 Article Series on the Serverless Framework!

Part 1: Serverless: Managing environment variables efficiently with stages

Part 2: Serverless: Managing config for different environments with S3 and Bash Scripts

Part 3: Serverless: Creating light and lean function packages

Part 4: Serverless: Breaking a large serverless.yml into manageable chunks

Part 5: Serverless: Reusing common configurations across functions

Engineering Manager @ Airlift Technologies