diff --git a/README.md b/README.md index fe9c3c6..df925c3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Serverless WarmUP Plugin ♨ Keep your lambdas warm during Winter. **Requirements:** -* Serverless *v1.12.x* or higher. +* Serverless *v1.12.x* or higher (Recommended *v1.33.x* or higher because of [this](https://github.com/FidelLimited/serverless-plugin-warmup/pull/69)). * AWS provider ## How it works @@ -17,26 +17,32 @@ WarmUP solves *cold starts* by creating one schedule event lambda that invokes a ## Setup - Install via npm in the root of your Serverless service: -``` + +### Installation + +Install via npm in the root of your Serverless service: + +```sh npm install serverless-plugin-warmup --save-dev ``` -* Add the plugin to the `plugins` array in your Serverless `serverless.yml`: +Add the plugin to the `plugins` array in your Serverless `serverless.yml`: ```yml plugins: - serverless-plugin-warmup ``` -* Add a `warmup.default` property to custom set the default configuration for all the functions +### Global configuration + +Add a `warmup.enabled` property to custom to enable/disable the warm up process by default for all the functions Enable WarmUp in general: ```yml custom: warmup: - default: true + enabled: true ``` For a specific stage: @@ -44,7 +50,7 @@ For a specific stage: ```yml custom: warmup: - default: production + enabled: production ``` For several stages: @@ -52,19 +58,22 @@ For several stages: ```yml custom: warmup: - default: + enabled: - production - staging ``` -* You can override the default `warmup` property on any function. +#### Function-specific configuration + +You can override the global `enabled` configuration on any function. Enable WarmUp for a specific function ```yml functions: hello: - warmup: true + warmup: + enabled: true ``` For a specific stage: @@ -72,7 +81,8 @@ For a specific stage: ```yml functions: hello: - warmup: production + warmup: + enabled: production ``` For several stages: @@ -81,24 +91,82 @@ For several stages: functions: hello: warmup: - - production - - staging + enabled: + - production + - staging ``` -Do not warm-up a function if `default` is set to true: +Do not warm-up a function if `enabled` is set to false: ```yml custom: warmup: - default: true + enabled: true ... functions: hello: - warmup: false + warmup: + enabled: false ``` -* WarmUP requires some permissions to be able to `invoke` lambdas. +### Other Options + +#### Global options + +* **folderName** (default `_warmup`) +* **cleanFolder** (default `true`) +* **name** (default `${service}-${stage}-warmup-plugin`) +* **role** (default to role in the provider) +* **tags** (default to serverless default tags) +* **schedule** (default `rate(5 minutes)`) - More examples [here](https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html). +* **memorySize** (default `128`) +* **timeout** (default `10` seconds) +* **prewarm** (default `false`) + +#### Options that can be overridden per function + +* **enabled** (default `false`) +* **source** (default `{ "source": "serverless-plugin-warmup" }`) +* **sourceRaw** (default `false`) + +```yml +custom: + warmup: + enabled: true // Whether to warm up functions by default or not + folderName: '_warmup' // Name of the folder created for the generated warmup + cleanFolder: false + memorySize: 256 + name: 'make-them-pop' + role: myCustRole0 + tags: + Project: foo + Owner: bar + schedule: 'cron(0/5 8-17 ? * MON-FRI *)' // Run WarmUP every 5 minutes Mon-Fri between 8:00am and 5:55pm (UTC) + timeout: 20 + prewarm: true // Run WarmUp immediately after a deploymentlambda + source: '{ "source": "my-custom-payload" }' + sourceRaw: true // Won't JSON.stringify() the source, may be necessary for Go/AppSync deployments +``` + +**Options should be tweaked depending on:** +* Number of lambdas to warm up +* Day cold periods +* Desire to avoid cold lambdas after a deployment + +**Lambdas invoked by WarmUP will have event source `serverless-plugin-warmup` (unless otherwise specified above):** + +```json +{ + "Event": { + "source": "serverless-plugin-warmup" + } +} +``` + +### Permissions + +WarmUP requires some permissions to be able to `invoke` lambdas. ```yaml custom: @@ -189,7 +257,10 @@ provider: ``` If using pre-warm, the deployment user also needs a similar policy so it can run the WarmUp lambda. -* Add an early callback call when the event source is `serverless-plugin-warmup`. You should do this early exit before running your code logic, it will save your execution duration and cost: + +#### On the function side + +Add an early callback call when the event source is `serverless-plugin-warmup`. You should do this early exit before running your code logic, it will save your execution duration and cost: ```javascript module.exports.lambdaToWarm = function(event, context, callback) { @@ -216,61 +287,23 @@ if(context.custom.source === 'serverless-plugin-warmup'){ ``` +## Deployment -* All done! WarmUP will run on SLS `deploy` and `package` commands +Once everything is configured WarmUP will run on SLS `deploy`. -## Options - -* **default** (default `false`) -* **folderName** (default `_warmup`) -* **cleanFolder** (default `true`) -* **memorySize** (default `128`) -* **name** (default `${service}-${stage}-warmup-plugin`) -* **role** (default to role in the provider) -* **schedule** (default `rate(5 minutes)`) - More examples [here](https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html). -* **timeout** (default `10` seconds) -* **prewarm** (default `false`) -* **source** (default `{ "source": "serverless-plugin-warmup" }`) -* **sourceRaw** (default `false`) -* **tags** (default to serverless default tags) - -```yml -custom: - warmup: - default: true // Whether to warm up functions by default or not - folderName: '_warmup' // Name of the folder created for the generated warmup - cleanFolder: false - memorySize: 256 - name: 'make-them-pop' - role: myCustRole0 - schedule: 'cron(0/5 8-17 ? * MON-FRI *)' // Run WarmUP every 5 minutes Mon-Fri between 8:00am and 5:55pm (UTC) - timeout: 20 - prewarm: true // Run WarmUp immediately after a deploymentlambda - source: '{ "source": "my-custom-payload" }' - sourceRaw: true // Won't JSON.stringify() the source, may be necessary for Go/AppSync deployments - tags: - Project: foo - Owner: bar +```sh +serverless deploy ``` -**Options should be tweaked depending on:** -* Number of lambdas to warm up -* Day cold periods -* Desire to avoid cold lambdas after a deployment +## Packaging +WarmUp also runs on SLS `package`. -**Lambdas invoked by WarmUP will have event source `serverless-plugin-warmup` (unless otherwise specified above):** - -```json -{ - "Event": { - "source": "serverless-plugin-warmup" - } -} +If you are doing your own [package artifact](https://serverless.com/framework/docs/providers/aws/guide/packaging#artifact) set the `cleanFolder` option to `false` and run +```sh +serverless package ``` -## Artifact - -If you are doing your own [package artifact](https://serverless.com/framework/docs/providers/aws/guide/packaging#artifact) set option `cleanFolder` to `false` and run `serverless package`. This will allow you to extract the `warmup` NodeJS lambda file from the `_warmup` folder and add it in your custom artifact logic. +This will allow you to extract the `warmup` NodeJS lambda file from the `_warmup` folder and add it in your custom artifact logic. ## Gotchas diff --git a/src/index.js b/src/index.js index b74a7a2..8a3eb26 100644 --- a/src/index.js +++ b/src/index.js @@ -86,105 +86,89 @@ class WarmUP { * */ afterDeployFunctions () { this.configPlugin() - if (this.warmup.prewarm) { + + if (this.warmupOpts.prewarm) { return this.warmUpFunctions() } } + getGlobalConfig (possibleConfig, defaultOpts = {}) { + const folderName = (typeof possibleConfig.folderName === 'string') ? possibleConfig.folderName : '_warmup' + const pathFolder = path.join(this.serverless.config.servicePath, folderName) + + return { + folderName, + pathFolder, + pathFile: `${pathFolder}/index.js`, + pathHandler: `${folderName}/index.warmUp`, + cleanFolder: (typeof possibleConfig.cleanFolder === 'boolean') ? possibleConfig.cleanFolder : defaultOpts.cleanFolder, + name: (typeof possibleConfig.name === 'string') ? possibleConfig.name : defaultOpts.name, + role: (typeof possibleConfig.role === 'string') ? possibleConfig.role : defaultOpts.role, + tags: (typeof possibleConfig.tags === 'object') ? possibleConfig.tags : defaultOpts.tags, + schedule: (typeof possibleConfig.schedule === 'string') ? [possibleConfig.schedule] + : (Array.isArray(possibleConfig.schedule)) ? possibleConfig.schedule : defaultOpts.schedule, + memorySize: (typeof possibleConfig.memorySize === 'number') ? possibleConfig.memorySize : defaultOpts.memorySize, + timeout: (typeof possibleConfig.timeout === 'number') ? possibleConfig.timeout : defaultOpts.timeout, + prewarm: (typeof possibleConfig.prewarm === 'boolean') ? possibleConfig.prewarm : defaultOpts.prewarm + } + } + + getFunctionConfig (possibleConfig, defaultOpts) { + if (typeof possibleConfig === 'undefined') { + return defaultOpts + } + + if (typeof possibleConfig !== 'object') { + return Object.assign({}, defaultOpts, { enabled: possibleConfig }) + } + + // Keep backwards compatibility for now + if (possibleConfig.default) { + possibleConfig.enabled = possibleConfig.default + } + + return { + enabled: (typeof possibleConfig.enabled === 'boolean' || + typeof possibleConfig.enabled === 'string' || + Array.isArray(possibleConfig.enabled)) + ? possibleConfig.enabled + : defaultOpts.enabled, + source: (typeof possibleConfig.source !== 'undefined') + ? (possibleConfig.sourceRaw ? possibleConfig.source : JSON.stringify(possibleConfig.source)) + : (defaultOpts.sourceRaw ? defaultOpts.source : JSON.stringify(defaultOpts.source)) + } + } + /** * @description Configure the plugin based on the context of serverless.yml * * @return {} * */ configPlugin () { - /** Set warm up folder, file and handler paths */ - this.folderName = '_warmup' - if (this.custom && this.custom.warmup && typeof this.custom.warmup.folderName === 'string') { - this.folderName = this.custom.warmup.folderName - } - this.pathFolder = this.getPath(this.folderName) - this.pathFile = this.pathFolder + '/index.js' - this.pathHandler = this.folderName + '/index.warmUp' - - /** Default options */ - this.warmup = { - default: false, + const globalDefaultOpts = { + folderName: '_warmup', cleanFolder: true, memorySize: 128, name: this.serverless.service.service + '-' + this.options.stage + '-warmup-plugin', schedule: ['rate(5 minutes)'], timeout: 10, - source: JSON.stringify({ source: 'serverless-plugin-warmup' }), prewarm: false } - /** Set global custom options */ - if (!this.custom || !this.custom.warmup) { - return - } - - /** Default warmup */ - if (typeof this.custom.warmup.default !== 'undefined') { - this.warmup.default = this.custom.warmup.default - } - - /** Clean folder */ - if (typeof this.custom.warmup.cleanFolder === 'boolean') { - this.warmup.cleanFolder = this.custom.warmup.cleanFolder - } - - /** Memory size */ - if (typeof this.custom.warmup.memorySize === 'number') { - this.warmup.memorySize = this.custom.warmup.memorySize - } - - /** Function name */ - if (typeof this.custom.warmup.name === 'string') { - this.warmup.name = this.custom.warmup.name + const functionDefaultOpts = { + enabled: false, + source: JSON.stringify({ source: 'serverless-plugin-warmup' }) } - /** Role */ - if (typeof this.custom.warmup.role === 'string') { - this.warmup.role = this.custom.warmup.role - } - - /** Tags */ - if (typeof this.custom.warmup.tags === 'object') { - this.warmup.tags = this.custom.warmup.tags - } - - /** Schedule expression */ - if (typeof this.custom.warmup.schedule === 'string') { - this.warmup.schedule = [this.custom.warmup.schedule] - } else if (Array.isArray(this.custom.warmup.schedule)) { - this.warmup.schedule = this.custom.warmup.schedule - } - - /** Timeout */ - if (typeof this.custom.warmup.timeout === 'number') { - this.warmup.timeout = this.custom.warmup.timeout - } - - /** Source */ - if (typeof this.custom.warmup.source !== 'undefined') { - this.warmup.source = this.custom.warmup.sourceRaw ? this.custom.warmup.source : JSON.stringify(this.custom.warmup.source) - } - - /** Pre-warm */ - if (typeof this.custom.warmup.prewarm === 'boolean') { - this.warmup.prewarm = this.custom.warmup.prewarm - } - } + const customConfig = (this.custom && typeof this.custom.warmup !== 'undefined') + ? this.custom.warmup + : {} - /** - * @description After create deployment artifacts - * - * @param {string} file — File path - * - * @return {String} Absolute file path - * */ - getPath (file) { - return path.join(this.serverless.config.servicePath, file) + /** Set global custom options */ + this.warmupOpts = Object.assign( + this.getGlobalConfig(customConfig, globalDefaultOpts), + this.getFunctionConfig(customConfig, functionDefaultOpts) + ) } /** @@ -196,10 +180,10 @@ class WarmUP { * @return {Promise} * */ cleanFolder () { - if (!this.warmup.cleanFolder) { + if (!this.warmupOpts.cleanFolder) { return Promise.resolve() } - return fs.removeAsync(this.pathFolder) + return fs.removeAsync(this.warmupOpts.pathFolder) } /** @@ -213,59 +197,47 @@ class WarmUP { createWarmer () { /** Get functions */ const allFunctions = this.serverless.service.getAllFunctions() + .map(functionName => this.serverless.service.getFunction(functionName)) + .map(functionConfig => ({ + name: functionConfig.name, + config: this.getFunctionConfig(functionConfig.warmup, this.warmupOpts) + })) /** Filter functions for warm up */ - return BbPromise.filter(allFunctions, (functionName) => { - const functionObject = this.serverless.service.getFunction(functionName) - - const enable = (config) => config === true || + const functionsToWarmup = allFunctions.filter((func) => { + const config = func.config.enabled + return config === true || config === this.options.stage || (Array.isArray(config) && config.indexOf(this.options.stage) !== -1) - - const functionConfig = functionObject.hasOwnProperty('warmup') - ? functionObject.warmup - : this.warmup.default - - /** Function needs to be warm */ - return enable(functionConfig) - }).then((functionNames) => { - /** Skip writing if no functions need to be warm */ - if (!functionNames.length) { - /** Log no warmup */ - this.serverless.cli.log('WarmUP: no lambda to warm') - return true - } - - /** Write warm up function */ - return this.createWarmUpFunctionArtifact(functionNames) - }).then((skip) => { - /** Add warm up function to service */ - if (skip !== true) { - return this.addWarmUpFunctionToService() - } }) + + /** Skip writing if no functions need to be warm */ + if (!functionsToWarmup.length) { + this.serverless.cli.log('WarmUP: no lambda to warm') + return Promise.resolve() + } + + /** Write warm up function */ + return this.createWarmUpFunctionArtifact(functionsToWarmup) + .then(() => this.addWarmUpFunctionToService()) } /** * @description Write warm up ES6 function * - * @param {Array} functionNames - Function names + * @param {Array} functions - Functions to be warmed up * * @fulfil {} — Warm up function created * @reject {Error} Warm up error * * @return {Promise} * */ - createWarmUpFunctionArtifact (functionNames) { + createWarmUpFunctionArtifact (functions) { /** Log warmup start */ - this.serverless.cli.log('WarmUP: setting ' + functionNames.length + ' lambdas to be warm') + this.serverless.cli.log('WarmUP: setting ' + functions.length + ' lambdas to be warm') - /** Get function names */ - functionNames = functionNames.map((functionName) => { - const functionObject = this.serverless.service.getFunction(functionName) - this.serverless.cli.log('WarmUP: ' + functionObject.name) - return functionObject.name - }) + /** Log functions being warmed up */ + functions.forEach(func => this.serverless.cli.log('WarmUP: ' + func.name)) const warmUpFunction = `"use strict"; @@ -273,17 +245,17 @@ class WarmUP { const aws = require("aws-sdk"); aws.config.region = "${this.options.region}"; const lambda = new aws.Lambda(); -const functionNames = ${JSON.stringify(functionNames)}; +const functions = ${JSON.stringify(functions)}; module.exports.warmUp = async (event, context, callback) => { console.log("Warm Up Start"); - const invokes = await Promise.all(functionNames.map(async (functionName) => { + const invokes = await Promise.all(functions.map(async (func) => { const params = { - ClientContext: "${Buffer.from(`{"custom":${this.warmup.source}}`).toString('base64')}", - FunctionName: functionName, + ClientContext: Buffer.from(\`{"custom":\${func.config.source}}\`).toString('base64'), + FunctionName: func.name, InvocationType: "RequestResponse", LogType: "None", Qualifier: process.env.SERVERLESS_ALIAS || "$LATEST", - Payload: '${this.warmup.source}' + Payload: func.config.source }; try { @@ -300,7 +272,7 @@ module.exports.warmUp = async (event, context, callback) => { }` /** Write warm up file */ - return fs.outputFileAsync(this.pathFile, warmUpFunction) + return fs.outputFileAsync(this.warmupOpts.pathFile, warmUpFunction) } /** @@ -312,25 +284,25 @@ module.exports.warmUp = async (event, context, callback) => { /** SLS warm up function */ this.serverless.service.functions.warmUpPlugin = { description: 'Serverless WarmUP Plugin', - events: this.warmup.schedule.map(schedule => ({ schedule })), - handler: this.pathHandler, - memorySize: this.warmup.memorySize, - name: this.warmup.name, + events: this.warmupOpts.schedule.map(schedule => ({ schedule })), + handler: this.warmupOpts.pathHandler, + memorySize: this.warmupOpts.memorySize, + name: this.warmupOpts.name, runtime: 'nodejs8.10', package: { individually: true, exclude: ['**'], - include: [this.folderName + '/**'] + include: [this.warmupOpts.folderName + '/**'] }, - timeout: this.warmup.timeout + timeout: this.warmupOpts.timeout } - if (this.warmup.role) { - this.serverless.service.functions.warmUpPlugin.role = this.warmup.role + if (this.warmupOpts.role) { + this.serverless.service.functions.warmUpPlugin.role = this.warmupOpts.role } - if (this.warmup.tags) { - this.serverless.service.functions.warmUpPlugin.tags = this.warmup.tags + if (this.warmupOpts.tags) { + this.serverless.service.functions.warmUpPlugin.tags = this.warmupOpts.tags } /** Return service function object */ @@ -349,11 +321,11 @@ module.exports.warmUp = async (event, context, callback) => { this.serverless.cli.log('WarmUP: Pre-warming up your functions') const params = { - FunctionName: this.warmup.name, + FunctionName: this.warmupOpts.name, InvocationType: 'RequestResponse', LogType: 'None', Qualifier: process.env.SERVERLESS_ALIAS || '$LATEST', - Payload: this.warmup.source + Payload: this.warmupOpts.source } return this.provider.request('Lambda', 'invoke', params)