Issues with python and the serverless framework - python-3.x

I am trying to understand how to setup multiple python lambdas and a step function within one single serverless.yml with each python lambda having its own dependencies. All of my lambda functions collaborate in the context of a step function for a shared common goal. With this rationale, it makes sense to me to put all of the code under one serverless.yml file.
As part of my MANY hours of trial and error and reading I found about the serverless-python-requirements plugin for The Serverless Framework that helps in packaging python functions that rely on OS-specific python libraries and also allow the separation of multiple requirements.txt in case different lambdas require different dependencies.
So at this point my problem is that the generated package is not including the dependencies that I provide in the requirements.txt whenever each function has its own requirements.txt
These are my artifacts:
package.json
{
"engines": {
"node": ">=10.0.0",
"npm": ">=6.0.0"
},
"name": "example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"serverless-python-requirements": "^5.1.0"
},
"devDependencies": {
"serverless": "^1.72.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "ISC"
}
serverless.yml
service: example
frameworkVersion: ">=1.72.0 <2.0.0"
plugins:
- serverless-python-requirements
custom:
stage: "${opt:stage, env:SLS_STAGE, 'local'}"
log_level: "${env:LOG_LEVEL, 'INFO'}"
pythonRequirements:
dockerizePip: true
provider:
name: aws
# profile: ${self:custom.profile}
stage: ${self:custom.stage}
runtime: python3.8
environment:
LOG_LEVEL: ${self:custom.log_level}
package:
individually: true
exclude:
- ./**
include:
- vendored/**
functions:
function1:
# module: folder1
handler: folder1/function1.handler
package:
include:
- 'folder1/**'
memorySize: 128
timeout: 60
function2:
# module: folder2
handler: folder2/function2.handler
package:
include:
- 'folder2/**'
memorySize: 128
timeout: 60
finally, my 2 python lambda functions are in separate folders and one of them requires specific dependencies:
folder1
function1.py
requirements.txt
folder2
function1.py
function1.py
import json
import logging
import os
import sys
import pyjokes
log_level = os.environ.get('LOG_LEVEL', 'INFO')
logging.root.setLevel(logging.getLevelName(log_level))
_logger = logging.getLogger(__name__)
class HandlerBaseError(Exception):
'''Base error class'''
class ComponentIdentifierBaseError(HandlerBaseError):
'''Base Component Identifier Error'''
def handler(event, context):
'''Function entry'''
_logger.debug('Event received: {}'.format(json.dumps(event)))
body = {
"message": "Go Serverless v1.0! Your function executed successfully!",
"joke":pyjokes.get_joke()
}
resp = {
'status': 'OK',
"body": json.dumps(body)
}
_logger.debug('Response: {}'.format(json.dumps(resp)))
return resp
if __name__ == "__main__":
handler('', '')
requirements.txt
pyjokes==0.6.0
function2.py
import json
import logging
import os
import sys
log_level = os.environ.get('LOG_LEVEL', 'INFO')
logging.root.setLevel(logging.getLevelName(log_level))
_logger = logging.getLogger(__name__)
class HandlerBaseError(Exception):
'''Base error class'''
class ElasticSearchPopulatorBaseError(HandlerBaseError):
'''Base Component Identifier Error'''
def handler(event, context):
'''Function entry'''
_logger.debug('Event received: {}'.format(json.dumps(event)))
resp = {
'status': 'OK'
}
_logger.debug('Response: {}'.format(json.dumps(resp)))
return resp
Note: I did try using the module+handler keywords in the serverless.xml as recommended on this link: ttps://github.com/UnitedIncome/serverless-python-requirements without any success
Something that I noted is that if I use the module+handler as follows:
functions:
function1:
module: folder1
handler: function1.handler
package:
include:
- 'folder1/**'
memorySize: 128
timeout: 60
Then, when I try running the function locally using: serverless invoke local -f function1 --log I get an error saying:
ModuleNotFoundError: No module named 'function1'
Also, if anyone has an example of multiple lambdas with different requirements.txt that works I would be very gratelful, ideally something just different than the typical hello world examples, the hello worlds all work very well for me ;), but in scenarios like this one where I would like to setup common libraries, have different dependencies, etc, and use one common serverless.yml things seem to fall apart. Again, my opinion is that these lambdas will operate together under one step function umbrella so there's strong cohesion here and I think that their build and deployment should happen under one common serverless service.

I recently developed a similar application using the serverless-python-requirements plugin that encapsulates multiple lambdas as part of one stack, and I was receiving ModuleNotFoundError whilst invoking the lambda function locally, yet it would work remotely; however, when I removed the module parameter from my serverless.yml file I was able to invoke locally but then it broke for remote executions.
I've been able to find a workaround by setting a path prefix in my serverless.yml:
functions:
LambdaTest:
handler: ${env:HANDLER_PATH_PREFIX, ""}handler.handler
module: src/test_lambda
package:
include:
- src/test_lambda/**
When I invoke the function locally, I prepend the environment variable to my command:
HANDLER_PATH_PREFIX=src/test_lambda/ ./node_modules/serverless/bin/serverless.js invoke local -f LambdaTest -p ./tests/resources/base_event.yml
I don't include the environment variable when invoking the function in AWS.
In order for this to work, I needed to add an init.py file to the root directory where my lambda function resides with the following code (taken from this solution) so that any modules I'm including in the code that exist in the lambda's directory (e.g. some_module -- see directory tree below):
import os
import sys
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
My lambda's directory structure:
src/
└── test_lambda
├── __init__.py <=== add the code to this one
├── handler.py
├── requirements.txt
└── some_module
├── __init__.py
└── example.py
As for your question regarding lambdas that use different requirements.txt files -- I use the individually parameter, like so:
package:
individually: true
include:
- infra/**
exclude:
- "**/*"
Within each requirements.txt for each lambda I refer to a separate requirements.txt file that resides in the base directory of my project using the -r option -- this file contains libraries that are common to all lambdas, so when Serverless is installing packages for each lambda it'll also include packages included in my ./requirements.txt file too.
I've included this solution in an issue regarding the serverless-python-requirements plugin in GitHub which would be worth keeping an eye on should this behaviour of the module parameter turns out to be a bug.
Hope it helps and let me know if you require clarification on anything.

Related

Sharing node_modules folder between lambda using Lambda Layers + Cloud Formation

I have a project that uses serverless-framework (this) to define the AWS resources to be used. I have the various .yml files that describe each resource that the project needs to run.
Recently, I've had to install several NPM packages for my lambdas and they've become very large in megabytes (>3MB), so the code is no longer viewable from the console.
Since including node_modules in each lambda is not a best practice and they are very heavy this way, I was wondering about using a Lambda Layer to share node_modules between lambdas.
As .yml I have a shared structure between all of them called provider.yml, something like:
name: aws
runtime: nodejs14.x
lambdaHashingVersion: 20201221
region: ${opt:region, "eu-central-1"}
stage: ${opt:stage, "dev"}
profile: ${self:custom.profiles.${self:provider.stage}}
deploymentBucket:
name: avatar-${self:provider.stage}-deploymentbucket
versioning: true
blockPublicAccess: true
environment:
EXAMPLE_ENV_VAR: ${self:custom.baseResource}-envvar
USERPOOL:
'Fn::ImportValue': ${self:custom.baseResource}-userPoolId
APP_CLIENT_ID:
'Fn::ImportValue': ${self:custom.baseResource}-userPoolClientId
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- ...
Resource: "*"
Then I have a file that includes the provider.yml and all the lambdas (called serverless.yml) and is the file that I use to deploy:
service: listOfLambdas
app: appname
frameworkVersion: '2'
projectDir: ../../
provider: ${file(../../resources/serverless-shared-config/provider.yml)}
package:
individually: true
exclude:
- "**/*"
functions:
- ${file(./serverless-functions.yml)}
Finally, I have the serverless-functions.yml that contains the Lambdas structure:
lambdaName:
handler: src/handlers/auth/example.run
name: ${self:custom.base}lambdaName
description: Lambda description
events:
- httpApi:
method: POST
path: /api/v1/example
package:
include:
- ../../node_modules/**
This includes the node_modules folder in the Lambda.
How can I create a resource with a YML template managed by Cloud Formation to create a Lambda Layer to which I can assign all my lambdas so that they share the node_modules folder. I expect to have to create a new serveless.yml inside the resources folder with the CloudFormation YML template to bind I guess somehow to my lambdas.
I need it to be managed by CloudFormation, so I can have a common stack with all the resources used by the project and so I can deploy it at start-up.
Where should the node_modules then be foldered in the project? Now the project is something like:
Root
|_ lib
|_ node_modules
|_ resources
|_serverless-shared-config
|_provider.yml
|_s3
|_serverless.yml
|_apigateay
|_ ...
|_ services
|_ lambda
|_ src
|_ index.js
|_ ...
|_ serverless.yml
|_ serverless-functions.yml
Where can I find a template in order to solve this problem? Or what is the best practice in order to share the node_modules between Lambdas?
Generally speaking, using Lambda Layers is a good approach to store shared code as you can reduce redundancy and keep the size of deployment packages small.
Layers can be created using the Serverless framework as well, so you can include them into your existing templates and have them managed by Serverless/CloudFormation. Here is a link to the documentation with configuration examples and explanations. Here is one example of how to add a layer to your serverless.yaml file:
layers:
NodeModules:
path: layer
package:
artifact: layer.zip
name: layer
compatibleRuntimes:
- nodejs12.x
- nodejs14.x
functions:
- ${file(./serverless-functions.yml)}
And then from each Lambda function in your serverless-functions.yaml file, you can refer to the layer as follows:
lambdaName:
handler: src/handlers/auth/example.run
name: ${self:custom.base}lambdaName
layers:
- { Ref: NodeModulesLambdaLayer }

AWS Lambda packaging with dependencies

Further outlining is in the context of NodeJS and Monorepo (based on Lerna).
I have AWS stack with several AWS Lambda inside deployed by means of AWS CloudFormation. Some of the lambdas are simple (the single small module) and could be inlined:
https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-lambda.Code.html#static-from-wbr-inlinecode
const someLambda = new Function(this, 'some-lambda', {
code: Code.fromInline(fs.readFileSync(require.resolve(<relative path to lambda module>), 'utf-8')),
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X
});
Some have no dependencies and packaged as follows:
const someLambda = new Function(this, 'some-lambda', {
code: Code.fromAsset(<relative path to folder with lambda>),
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X
});
But in case of relatively huge lambdas with dependencies, as I understand, we only way to package (proposed by API) is #aws-cdk/aws-lambda-nodejs:
import * as lambdaNJS from "#aws-cdk/aws-lambda-nodejs";
export function createNodeJSFunction(
scope: cdk.Construct, id: string, nodejsFunctionProps: Partial<NodejsFunctionProps>
) {
const params: NodejsFunctionProps = Object.assign({
parcelEnvironment: { NODE_ENV: 'production' },
}, nodejsFunctionProps);
return new lambdaNJS.NodejsFunction(scope, id, params);
}
For standalone packages, it works well, but in case of the monorepo it just hangs on synth of the stack.
I just looking for alternatives, cause I believe it is not a good idea to bundle (parcel) BE sources.
I've created the following primitive library to zip only required node_modules despite packages hoisting.
https://github.com/redneckz/slice-node-modules
Usage (from monorepo root):
$ npx #redneckz/slice-node-modules \
-e packages/some-lambda/lib/index.js \
--exclude 'aws-*' \
--zip some-lambda.zip
--exclude 'aws-*' - AWS runtime is included by default, so no need to package it.
Here is an example if using cloudformation and template.yml.
Create a make file: Makefile with following targets
# Application
APPLICATION=applicatin-name
# AWS
PROFILE=your-profile
REGION=us-east-1
S3_BUCKET=${APPLICATION}-deploy
install:
rm -rf node_modules
npm install
clean:
rm -rf build
build: clean
mkdir build
zip -qr build/package.zip src node_modules
ls -lah build/package.*
deploy:
sam package \
--profile ${PROFILE} \
--region ${REGION} \
--template-file template.yaml \
--s3-bucket ${S3_BUCKET} \
--output-template-file ./build/package.yaml
sam deploy \
--profile ${PROFILE} \
--region ${REGION} \
--template-file ./build/package.yaml \
--stack-name ${APPLICATION}-lambda \
--capabilities CAPABILITY_NAMED_IAM
Make sure the s3 bucket is created, you could add this step as another target in the Makefile.
How to build and deploy on AWS ?
make build
make deploy
I have struggled with this as well, and I was using your slice-node-modules successfully for a while. As I have consolidated more of my projects into monorepos and begun using shared dependencies which reside as siblings rather than being externally published, I ran into shortcomings with that approach.
I've created a new tool called lerna-to-lambda which was specifically tailored to my use case. I published it publicly with minimal documentation, hopefully enough to help others in similar situations. The gist of it is that you run l2l in your bundling step, after you've installed all of your dependencies, and it copies what is needed into an output directory which is then ready to deploy to Lambda using SAM or whatever.
For example, from the README, something like this might be in your Lambda function's package.json:
"scripts": {
...
"clean": "rimraf build lambda",
"compile": "tsc -p tsconfig.build.json",
"package": "l2l -i build -o lambda",
"build": "yarn run clean && yarn run compile && yarn run package"
},
In this case, the compile step is compiling TypeScript files from a source directory into JavaScript files in the build directory. Then the package step bundles up all the code from build along with all of the Lambda's dependencies (except aws-sdk) into the directory lambda, which is what you'd deploy to AWS. If someone were using plain JavaScript rather than TypeScript, they could just copy the necessary .js files into the build directory before packaging.
It's likely that your solution is still working fine for your needs, but I thought I would share this here as an alternative in case others are in a similar situation and have trouble using slice-node-modules.

How to setup serverless.yml and webpack.config for a multiple-runtime AWS Lambda service

I want to deploy AWS Lambda functions with Node8.10 and Ruby2.5 runtimes from one serverless.yml file.
I set up the following folder structure, with /node and /ruby holding my respective handlers.
-/nodeRubyLambdas
-/node
-handler.js
-package.json, package-lock.json, /node_modules
-/ruby
-rubyRijndaelEncryption.rb
-Gemfile, Gemfile.lock, /vendor
-serverless.yml
-webpack.config.js
-package.json for serverless-webpack
Here is my serverless.yml
service: nodeRubyLambdas
plugins:
- serverless-webpack
- serverless-offline
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
provider:
name: aws
stage: dev
region: us-west-2
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "*"
package:
individually: true
functions:
nodeMain:
handler: node/handler.main
runtime: nodejs8.10
events:
- http:
path: main
method: get
package:
individually: true
rubyEncryption:
handler: ruby/rubyRijndaelEncryption.lambda_handler
runtime: ruby2.5
environment:
RIJNDAEL_PASSWORD: 'a string'
package:
individually: true
My webpack configuration: (This is the base example, I just added the bit to ignore ruby files when I got my first error.)
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: slsw.lib.entries,
target: "node",
// Generate sourcemaps for proper error messages
devtool: 'source-map',
// Since 'aws-sdk' is not compatible with webpack,
// we exclude all node dependencies
externals: [nodeExternals()],
mode: slsw.lib.webpack.isLocal ? "development" : "production",
optimization: {
// We do not want to minimize our code.
minimize: false
},
performance: {
// Turn off size warnings for entry points
hints: false
},
// Run babel on all .js files and skip those in node_modules
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: __dirname,
exclude: [/node_modules/, /\.rb$/]
}
]
}
};
Fail #0:
[Webpack Compilation error] Module parse failed
Fail #1:
Basically, webpack assumes all functions are .js and tries to package them as such. Based off this suggestion, I forced my entry point in webpack config to be my handler.js
module.exports = {
entry: "./node/handler.js",
target: "node",
...
This packages ONLY the Node Lambda. An empty placeholder for the Ruby Lambda is created on AWS.
Fail #2:
I commented out webpack from serverless.yml and added include and exclude statements in the functions package options.
functions:
nodeMain:
package:
individually: true
include:
- node/**
- handler.js
exclude:
- ruby/**
- rubyLambda/**
rubyEncryption:
package:
individually: true
include:
- vendor/**
- rubyRijndaelEncryption.rb
exclude:
- Gemfile
- Gemfile.lock
- node/**
This gets an [ENOENT: no such file or directory] for node/node_modules/#babel/core/node_modules/.bin/parser. This file is not there but I don't understand why it is looking for it, since webpack is not being called.
Sort of success?:
I was able to get the Lambdas to deploy if I commented out webpack and used
serverless deploy function -f <function name here>
to deploy the Ruby Lambda and then uncommented webpack and used the same thing to deploy the Node Lambda.
I'm convinced that there's a better way to get them to deploy; Have I missed something in my setup? Is there another option I haven't tried?
P.S. I did see this pull request https://github.com/serverless-heaven/serverless-webpack/pull/256, but it seems to be abandoned since 2017.
serverless-webpack is not designed for non-JS runtimes. It hijacks serverless packaging and deploys ONLY the webpack output.
Here are your options:
Don't use serverless-webpack and simply use serverless' built-in packaging.
You can use webpack directly (not serverless-webpack), and change your build process to compile using webpack first and then let serverless deploy the output folder.
P.S. The package.individually property is a root-level property in your serverless.yml. It shouldn't be in provider or in your function definitions.
For those who may be looking for options for multiple-runtimes other than serverless-webpack, I ended up switching to this plugin: https://www.npmjs.com/package/serverless-plugin-include-dependencies.
It works with my runtimes (Ruby and Node) and lets you use package.individually with package.include/exclude at the root and function level if the plugin misses something.

Serverless not including my node_modules

I have a nodejs serverless project that has this structure:
-node_modules
-package.json
-serverless.yml
-funcitons
-medium
-mediumHandler.js
my serverless.yml:
service: googleAnalytic
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: us-east-1
package:
include:
- node_modules/**
functions:
mediumHandler:
handler: functions/medium/mediumHandler.mediumHandler
events:
- schedule:
name: MediumSourceData
description: 'Captures data between set dates'
rate: rate(2 minutes)
- cloudwatchEvent:
event:
source:
- "Lambda"
detail-type:
- ""
- cloudwatchLog: '/aws/lambda/mediumHandler'
my sls info shows:
Service Information
service: googleAnalytic
stage: dev
region: us-east-1
stack: googleAnalytic-dev
api keys:
None
endpoints:
None
functions:
mediumHandler: googleAnalytic-dev-mediumHandler
When I run sls:
serverless invoke local -f mediumHandler
it works and my script where I included googleapis and aws-sdk work. But when I deploy, those functions are skipped and show no error.
When debugging serverless's packaging process, use sls package (or sls deploy --noDeploy (for old versions). You'll get a .serverless directory that you can inspect to see what's inside the deployment package.
From there, you can see if node_modules is included or not and make changes to your serverless.yml correspondingly without needing to deploy every time you make a change.
Serverless will exclude development packages by default. Check your package.json and ensure your required packages are in the dependencies object, as devDependencies will be excluded.
I was dumb to put this in my serverless.yml which caused me the same issue you're facing.
package:
patterns:
- '!node_modules/**'

Handler in subdirectory of AWS Lambda function not running

I'm getting an awfully unfortunate error on Lambda:
Unable to import module 'lib/index': Error
at require (internal/module.js:20:19)
Which is strange because there is definitely a function called handler getting exported from lib/index...not sure if the whole subdirectory thing has been an issue for others so I wanted to ask.
sam-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Does something crazy
Resources:
SomeFunction:
Type: AWS::Serverless::Function
Properties:
Handler: lib/index.handler
Role: arn:aws:iam::...:role/lambda-role
Runtime: nodejs6.10
Timeout: 10
Events:
Timer:
Type: Schedule
Properties:
Schedule: rate(1 minute)
Module structure
|-- lib
| `-- index.js
`-- src
`-- index.js
I have nested it here because I'm transpiling ES6 during my build process using the following, excerpt from package.json:
"build": "babel src -d lib"
buildspec.yaml
version: 0.1
phases:
install:
commands:
- npm install
- aws cloudformation package --template-file sam-template.yaml --s3-bucket some-bucket --output-template-file compiled-sam-template.yaml
build:
commands:
- npm run build
post_build:
commands:
- npm prune --production
artifacts:
files:
- 'node_modules/**/*'
- 'lib/**/*'
- 'compiled-template.yaml'
The aws cloudformation package command is shipping the built assets, which is run in the install phase of the shown code. Moving it to the post_build will ensure it captures everything needed, including the lib/index in question:
post_build:
commands:
- npm prune --production
- aws cloudformation package ...
You are trying to import lib/index which will try to find a package named lib as if you did npm install --save lib but you are most likely trying to import a file relative to your own project and you are not giving it a relative path in your import.
Change 'lib/index' to './lib/index' - or '../lib/index' etc. - depending where it is and see if it helps.
By the way, if you're trying to import the file lib/index.js and not a directory lib/import/ then you may use a shorter ./lib path, as in:
const lib = require('./lib');
Of course you didn't show even a single line of your code so I can only guess what you're doing.
Your handler should be .lib/index.handler considering your index.js file is in a subdirectory lib.
The reference to the handler must be relative to the lambda to be execute;
Ex:
if the file lambda is placed in the path:
x-lambda/yyy/lambda.py
the handler must be:
..yyy/lambda.lambda_handler
it suppose that in the lambda.py exist the function: lambda_handler()

Resources