Skip to content

Add AWS Lambda Layer Support #310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 7, 2019
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -32,6 +33,7 @@ class ServerlessPythonRequirements {
slimPatterns: false,
slimPatternsAppendDefaults: true,
zip: false,
layer: false,
cleanupZipHelper: true,
invalidateCaches: false,
fileName: 'requirements.txt',
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -170,6 +178,7 @@ class ServerlessPythonRequirements {
}
return BbPromise.bind(this)
.then(removeVendorHelper)
.then(layerRequirements)
.then(() =>
injectAllRequirements.bind(this)(
arguments[1].functionObj &&
Expand Down
5 changes: 5 additions & 0 deletions lib/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
90 changes: 90 additions & 0 deletions lib/layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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);

/**
* Get the paths for the compatible runtimes of the layer
* @param {string[]} list of runtime paths
*/
function getRunTimeBuildPaths() {
const runtimepaths = {
'python2.7': 'python',
'python3.6': path.join('python', 'lib', 'python3.6', 'site-packages'),
'python3.7': path.join('python', 'lib', 'python3.7', 'site-packages')
};

let runtimes = [];

// Defer to Layer config first
if (this.options.layer.compatibleRuntimes) {
runtimes = this.options.layer.compatibleRuntimes;
// If none provided, assume the provider runtime
} else if (this.serverless.service.provider.runtime) {
runtimes = [this.serverless.service.provider.runtime];
// If still no runtime found, just assume latest python
} else {
runtimes = ['python3.7'];
}

return BbPromise.resolve(runtimes.map(runtime => runtimepaths[runtime]));
}

/**
* Zip up requirements to be used as layer package.
* @param {string[]} list of paths where the requirements should be put in the layer
* @return {Promise} the JSZip object constructed.
*/
function zipRequirements(runtimepaths) {
const rootZip = new JSZip();
const src = path.join('.serverless', 'requirements');

return BbPromise.each(runtimepaths, runtimepath =>
addTree(rootZip.folder(runtimepath), src)
).then(() =>
writeZip(rootZip, path.join('.serverless', 'pythonRequirementsLayer.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(getRunTimeBuildPaths)
.then(zipRequirements)
.then(createLayers);
}

module.exports = {
layerRequirements
};