diff --git a/README.md b/README.md index f396dcb6..f0297b66 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,38 @@ custom: ``` This will remove all folders within the installed requirements that match the names in `slimPatterns` + +### Lamba Layer +Another method for dealing with large dependencies is to put them into a +[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html). +Simply add the `layer` option to the configuration. +```yaml +custom: + pythonRequirements: + layer: true +``` +The requirements will be zipped up and a layer will be created automatically. +Now just add the reference to the functions that will use the layer. +```yaml +functions: + hello: + handler: handler.hello + layers: + - {Ref: PythonRequirementsLambdaLayer} +``` +If the layer requires additional or custom configuration, add them onto the `layer` option. +```yaml +custom: + pythonRequirements: + layer: + name: ${self:provider.stage}-layerName + description: Python requirements lamba layer + compatibleRuntimes: + - python3.7 + licenseInfo: GPLv3 + allowedAccounts: + - '*' +``` ## Omitting Packages You can omit a package from deployment with the `noDeploy` option. Note that dependencies of omitted packages must explicitly be omitted too. By default, @@ -423,3 +455,4 @@ zipinfo .serverless/xxx.zip * [@andrewfarley](https://github.com/andrewfarley) - Implemented download caching and static caching * [@bweigel](https://github.com/bweigel) - adding the `slimPatternsAppendDefaults` option & fixing per-function packaging when some functions don't have requirements & Porting tests from bats to js! * [@squaresurf](https://github.com/squaresurf) - adding usePoetry option + * [@david-mk-lawrence](https://github.com/david-mk-lawrence) - added Lambda Layer support diff --git a/index.js b/index.js index dd20f7d7..b5200285 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ const { packRequirements } = require('./lib/zip'); const { injectAllRequirements } = require('./lib/inject'); +const { layerRequirements } = require('./lib/layer'); const { installAllRequirements } = require('./lib/pip'); const { pipfileToRequirements } = require('./lib/pipenv'); const { pyprojectTomlToRequirements } = require('./lib/poetry'); @@ -32,6 +33,7 @@ class ServerlessPythonRequirements { slimPatterns: false, slimPatternsAppendDefaults: true, zip: false, + layer: false, cleanupZipHelper: true, invalidateCaches: false, fileName: 'requirements.txt', @@ -96,6 +98,12 @@ class ServerlessPythonRequirements { }`; options.dockerImage = options.dockerImage || defaultImage; } + if (options.layer) { + // If layer was set as a boolean, set it to an empty object to use the layer defaults. + if (options.layer === true) { + options.layer = {}; + } + } return options; } @@ -170,6 +178,7 @@ class ServerlessPythonRequirements { } return BbPromise.bind(this) .then(removeVendorHelper) + .then(layerRequirements) .then(() => injectAllRequirements.bind(this)( arguments[1].functionObj && diff --git a/lib/inject.js b/lib/inject.js index 973ba99b..1abbb531 100644 --- a/lib/inject.js +++ b/lib/inject.js @@ -72,6 +72,11 @@ function moveModuleUp(source, target, module) { * @return {Promise} the combined promise for requirements injection. */ function injectAllRequirements(funcArtifact) { + if (this.options.layer) { + // The requirements will be placed in a Layer, so just resolve + return BbPromise.resolve(); + } + this.serverless.cli.log('Injecting required Python packages to package...'); if (this.serverless.service.package.individually) { diff --git a/lib/layer.js b/lib/layer.js new file mode 100644 index 00000000..e0a57358 --- /dev/null +++ b/lib/layer.js @@ -0,0 +1,60 @@ +const BbPromise = require('bluebird'); +const fse = require('fs-extra'); +const path = require('path'); +const JSZip = require('jszip'); +const { writeZip, addTree } = require('./zipTree'); + +BbPromise.promisifyAll(fse); + +/** + * Zip up requirements to be used as layer package. + * @return {Promise} the JSZip object constructed. + */ +function zipRequirements() { + const rootZip = new JSZip(); + const src = path.join('.serverless', 'requirements'); + const runtimepath = 'python'; + + return addTree(rootZip.folder(runtimepath), src).then(() => + writeZip(rootZip, path.join('.serverless', 'pythonRequirements.zip')) + ); +} + +/** + * Creates a layer on the serverless service for the requirements zip. + * @return {Promise} empty promise + */ +function createLayers() { + this.serverless.service.layers['pythonRequirements'] = Object.assign( + { + artifact: path.join('.serverless', 'pythonRequirements.zip'), + name: `${this.serverless.service.stage}-python-requirements`, + description: + 'Python requirements generated by serverless-python-requirements.', + compatibleRuntimes: [this.serverless.service.provider.runtime] + }, + this.options.layer + ); + + return BbPromise.resolve(); +} + +/** + * Creates a layer from the installed requirements. + * @return {Promise} the combined promise for requirements layer. + */ +function layerRequirements() { + if (!this.options.layer) { + return BbPromise.resolve(); + } + + this.serverless.cli.log('Packaging Python Requirements Lambda Layer...'); + + return BbPromise.bind(this) + .then(zipRequirements) + .then(createLayers); +} + +module.exports = { + layerRequirements +};