├── .gitignore ├── .jshintrc ├── README.md ├── index.js ├── lib └── AutoPrunePlugin.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.tern-project 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": true, 3 | "globalstrict": true, 4 | "curly": true, 5 | "futurehostile": true, 6 | "latedef": true, 7 | "esnext": true, 8 | "laxcomma": true, 9 | "mocha": true, 10 | "node": true, 11 | "eqeqeq": true, 12 | "undef": true, 13 | "unused": true, 14 | "-W100": true, 15 | "predef": [ 16 | "base_dir", 17 | "test_dir", 18 | "consumer_base_dir" 19 | ] 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-autoprune-plugin 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | 5 | This Serverless 0.5.x plugin deletes old AWS Lambda versions. 6 | 7 | 8 | ## Overview 9 | The plugin lets you delete old AWS Lambda versions from your account. 10 | 11 | `serverless-autoprune-plugin` is heavily inspired by Nopik's 12 | [serverless-lambda-prune-plugin](https://github.com/Nopik/serverless-lambda-prune-plugin) 13 | but adds some much needed functionality such as limiting pruning to a specific region, project and function. 14 | It is fully compatible with Serverless 0.5.5 and higher. 15 | 16 | 17 | ## Installation 18 | 19 | 1. Install the plugin module. 20 | 21 | `npm install serverless-autoprune-plugin --save` will install the latest version of the plugin. 22 | 23 | If you want to debug, you also can reference the source repository at a specific version or branch 24 | with `npm install https://github.com/arabold/serverless-autoprune-plugin#` 25 | 26 | 2. Activate the plugin in your Serverless project. 27 | 28 | Add `serverless-autoprune-plugin` to the plugins array in your `s-project.json`. 29 | ``` 30 | { 31 | "name": "my-project", 32 | "custom": {}, 33 | "plugins": [ 34 | "serverless-autoprune-plugin" 35 | ] 36 | } 37 | ``` 38 | 39 | 40 | ## Usage 41 | 42 | ### Function Deploy 43 | 44 | This plugin hooks into the Serverless `function deploy` command action and 45 | extends it with two additional options: 46 | 47 | * `-p|--prune`: Delete previous Lambda versions after deployment. 48 | * `-n|--number `: keep last N versions (defaults to 3). 49 | 50 | A simple example to deploy and prune all functions in your project: 51 | ``` 52 | serverless function deploy --all --prune 53 | ``` 54 | 55 | 56 | ### Function Prune 57 | 58 | If you only want to delete old versions without deploying a new one, use the new `prune` 59 | command action: 60 | ``` 61 | serverless function prune [OPTION]... [FUNCTION]... 62 | ``` 63 | 64 | You can specify one or multiple function names to prune, omit any function names to prune the 65 | functions in the current directory tree, or specify the `-a` or `--all` option to prune all 66 | functions of the project. 67 | 68 | Available options are: 69 | * `-s|--stage `: prune only a specific stage (only applicable if your Lambda 70 | functions use different names per stage) 71 | * `-r|--region `: prune only a specific region (defaults to all regions). 72 | * `-n|--number `: keep last N versions (defaults to 3). 73 | * `-a|--all`: prune all functions of the current project. 74 | 75 | 76 | ## Releases 77 | 78 | ### 0.2.1 79 | * You can run the plugin without specifying a target region now. It will automatically prune 80 | your Lambda functions in all deployed regions. 81 | 82 | ### 0.2.0 83 | * Finally the plugin deserves it's name: Auto-Pruning! Passing `--prune` to your 84 | `serverless function deploy` will automatically delete previous Lambda versions. 85 | * Handle AWS Rate Limit responses using a temporary fix until Serverless 0.5.6 is released 86 | 87 | ### 0.1.3 88 | * Support pruning only a specific stage (in case Lambda function names differ per stage) 89 | * Use Serverless credentials depending on stage and region specified. 90 | 91 | ### 0.1.2 92 | * Added `aws-sdk` as dependency in case it's not installed globally 93 | 94 | ### 0.1.1 95 | * Small bugfix 96 | 97 | ### 0.1.0 98 | * Initial release 99 | 100 | ### To Dos 101 | * Pruning of API Gateway Deployments (https://github.com/Nopik/serverless-lambda-prune-plugin/pull/6) 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Serverless Auto-Prune Plugin 5 | */ 6 | 7 | module.exports = require('./lib/AutoPrunePlugin'); 8 | -------------------------------------------------------------------------------- /lib/AutoPrunePlugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Action: Function Prune 5 | */ 6 | 7 | module.exports = function(S) { 8 | 9 | const SError = require(S.getServerlessPath('Error')), 10 | SCli = require(S.getServerlessPath('utils/cli')), 11 | SUtils = S.utils, 12 | BbPromise = require('bluebird'), 13 | _ = require('lodash'); 14 | 15 | // TODO This can be removed once Serverless 0.5.6 has been released 16 | function persistentRequest(f) { 17 | return new BbPromise(function(resolve, reject){ 18 | let doCall = function(){ 19 | f() 20 | .then(resolve) 21 | .catch(function(err) { 22 | 23 | if (err.statusCode === 429) { 24 | S.utils.sDebug("'Too many requests' received, sleeping 5 seconds"); 25 | setTimeout( doCall, 5000 ); 26 | } 27 | else { 28 | reject( err ); 29 | } 30 | }); 31 | }; 32 | return doCall(); 33 | }); 34 | } 35 | 36 | 37 | /** 38 | * Serverless Auto-Prune Plugin 39 | */ 40 | 41 | class AutoPrunePlugin extends S.classes.Plugin { 42 | 43 | /** 44 | * Get Name 45 | */ 46 | 47 | static getName() { 48 | return 'serverless.core.' + AutoPrunePlugin.name; 49 | } 50 | 51 | /** 52 | * Constructor 53 | */ 54 | 55 | constructor() { 56 | super(); 57 | S.commands.function.deploy.options.push({ 58 | option: 'prune', 59 | shortcut: 'p', 60 | description: 'Optional - Delete previous Lambda versions after deployment' 61 | }); 62 | S.commands.function.deploy.options.push({ 63 | option: 'number', 64 | shortcut: 'n', 65 | description: 'Optional - Keep last N versions (default: 3)' 66 | }); 67 | } 68 | 69 | /** 70 | * Register Plugin Actions 71 | */ 72 | 73 | registerActions() { 74 | 75 | S.addAction(this._functionPruneAction.bind(this), { 76 | handler: 'functionPrune', 77 | description: 'Delete old/unused Lambda versions from your AWS account', 78 | context: 'function', 79 | contextAction: 'prune', 80 | options: [{ 81 | option: 'stage', 82 | shortcut: 's', 83 | description: 'Optional if Lambda function names don´t differ by stage' 84 | }, { 85 | option: 'region', 86 | shortcut: 'r', 87 | description: 'Optional - Target one region to prune' 88 | }, { 89 | option: 'all', 90 | shortcut: 'a', 91 | description: 'Optional - Deploy all Functions' 92 | }, { 93 | option: 'number', 94 | shortcut: 'n', 95 | description: 'Optional - Keep last N versions (default: 3)' 96 | }], 97 | parameters: [{ 98 | parameter: 'names', 99 | description: 'One or multiple function names', 100 | position: '0->' 101 | }] 102 | }); 103 | 104 | return BbPromise.resolve(); 105 | } 106 | 107 | /** 108 | * Register Hooks 109 | */ 110 | 111 | registerHooks() { 112 | 113 | S.addHook(this._functionDeployPostHook.bind(this), { 114 | action: 'functionDeploy', 115 | event: 'post' 116 | }); 117 | 118 | return BbPromise.resolve(); 119 | } 120 | 121 | /** 122 | * Function Prune Action 123 | */ 124 | 125 | _functionPruneAction(evt) { 126 | 127 | let _this = this; 128 | _this.evt = evt; 129 | 130 | // Flow 131 | return BbPromise.try(() => { 132 | if (!S.getProject().getAllStages().length) { 133 | return BbPromise.reject(new SError('No existing stages in the project')); 134 | } 135 | }) 136 | .bind(_this) 137 | .then(_this._validateAndPrepare) 138 | .then(_this._pruneFunctions) 139 | .then(function() { 140 | 141 | // Status 142 | if (_this.pruned) { 143 | 144 | // Line for neatness 145 | SCli.log('------------------------'); 146 | 147 | SCli.log('Successfully pruned the following functions in the following regions: '); 148 | 149 | // Display Functions & ARNs 150 | for (let i = 0; i < Object.keys(_this.pruned).length; i++) { 151 | let region = _this.pruned[Object.keys(_this.pruned)[i]]; 152 | SCli.log(Object.keys(_this.pruned)[i] + ' ------------------------'); 153 | for (let j = 0; j < region.length; j++) { 154 | SCli.log(` ${region[j].functionName} (${region[j].lambdaName}): ${region[j].deleted} versions deleted`); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Return EVT 161 | */ 162 | 163 | return _this.evt; 164 | 165 | }); 166 | } 167 | 168 | /** 169 | * Function Deployment Post Hook 170 | */ 171 | 172 | _functionDeployPostHook(evt) { 173 | if (evt.options.prune) { 174 | 175 | // Line for neatness 176 | SCli.log('------------------------'); 177 | 178 | return this._functionPruneAction(evt); 179 | } 180 | else { 181 | return evt; 182 | } 183 | } 184 | 185 | /** 186 | * Validate And Prepare 187 | * - If CLI, maps CLI input to event object 188 | */ 189 | 190 | _validateAndPrepare() { 191 | 192 | let _this = this; 193 | 194 | // Set Defaults 195 | _this.functions = []; 196 | _this.evt.options.stage = _this.evt.options.stage ? _this.evt.options.stage : null; 197 | _this.evt.options.names = _this.evt.options.names ? _this.evt.options.names : []; 198 | _this.evt.options.number = (_this.evt.options.number !== null) ? _this.evt.options.number : 3; 199 | 200 | // Instantiate Classes 201 | _this.aws = S.getProvider(); 202 | _this.project = S.getProject(); 203 | 204 | // Set and check deploy Regions (check for undefined as region could be "false") 205 | if (_this.evt.options.region && S.getProvider().validRegions.indexOf(_this.evt.options.region) <= -1) { 206 | return BbPromise.reject(new SError('Invalid region specified')); 207 | } 208 | 209 | _this.regions = []; 210 | if (_this.evt.options.region) { 211 | _this.regions.push(_this.evt.options.region); 212 | } 213 | else { 214 | // Get a list of all regions of all stages 215 | let stages = S.getProject().getAllStages(); 216 | stages.forEach((stage) => { 217 | _this.regions = _.union(_this.regions, S.getProject().getAllRegionNames(stage.name)); 218 | }); 219 | } 220 | 221 | if (_this.evt.options.names.length) { 222 | _this.evt.options.names.forEach((name) => { 223 | let func = _this.project.getFunction(name); 224 | if (!func) { 225 | throw new SError(`Function "${name}" doesn't exist in your project`); 226 | } 227 | _this.functions.push(_this.project.getFunction(name)); 228 | }); 229 | } 230 | 231 | // If CLI and no function names targeted, prune from CWD 232 | if (S.cli && 233 | !_this.evt.options.names.length && 234 | !_this.evt.options.all) { 235 | _this.functions = SUtils.getFunctionsByCwd(S.getProject().getAllFunctions()); 236 | } 237 | 238 | // If --all is selected, load all paths 239 | if (_this.evt.options.all) { 240 | _this.functions = S.getProject().getAllFunctions(); 241 | } 242 | 243 | if (_this.functions.length === 0) { 244 | throw new SError(`You don't have any functions in your project`); 245 | } 246 | 247 | return BbPromise.resolve(); 248 | } 249 | 250 | /** 251 | * Prune Functions 252 | */ 253 | 254 | _pruneFunctions() { 255 | 256 | // Status 257 | SCli.log(`Pruning specified functions in the following regions: ${this.regions.join(', ')}`); 258 | 259 | this._spinner = SCli.spinner(); 260 | this._spinner.start(); 261 | 262 | return BbPromise 263 | // Deploy Function Code in each region 264 | .each(this.regions, (region) => this._pruneByRegion(region)) 265 | .then(() => S.utils.sDebug(`pruning is done`)) 266 | // Stop Spinner 267 | .then(() => this._spinner.stop(true)); 268 | } 269 | 270 | /** 271 | * Prune By Region 272 | */ 273 | 274 | _pruneByRegion(region) { 275 | let _this = this; 276 | 277 | const pruneFunc = (func) => { 278 | // Determine function names per stage as they might differ 279 | let stages = S.getProject().getAllStages(); 280 | let lambdaNames = []; 281 | stages.forEach((stage) => { 282 | if (!_this.evt.options.stage || _this.evt.options.stage === stage.name) { 283 | let lambdaName = func.getDeployedName({ 284 | stage: stage.name, 285 | region: region 286 | }); 287 | if (lambdaNames.indexOf(lambdaName) < 0) { 288 | lambdaNames.push(lambdaName); 289 | } 290 | } 291 | }); 292 | 293 | return BbPromise.each(lambdaNames, (lambdaName) => { 294 | return BbPromise.join( 295 | persistentRequest( () => _this.aws.request('Lambda', 'listAliases', { FunctionName: lambdaName }, _this.evt.options.stage, region) ), 296 | persistentRequest( () => _this.aws.request('Lambda', 'listVersionsByFunction', { FunctionName: lambdaName }, _this.evt.options.stage, region) ) 297 | ).spread((aliases, versions) => { 298 | S.utils.sDebug( `Pruning ${func.name}, found ${aliases.Aliases.length} aliases and ${versions.Versions.length} versions` ); 299 | 300 | // Keep all named versions 301 | let keepVersions = aliases.Aliases.map((a) => { 302 | return a.FunctionVersion; 303 | }); 304 | 305 | // Always keep the latest version 306 | keepVersions.push('$LATEST'); 307 | 308 | // Sort versions so we keep the newest ones 309 | let vs = versions.Versions.sort((v1,v2) => { 310 | if (v1.LastModified < v2.LastModified) { 311 | return 1; 312 | } 313 | else if (v1.LastModified > v2.LastModified) { 314 | return -1; 315 | } 316 | else { 317 | return 0; 318 | } 319 | }); 320 | 321 | // Keep the last N versions 322 | let toKeep = _this.evt.options.number; 323 | vs.forEach((v) => { 324 | if ((toKeep > 0) && (keepVersions.indexOf(v.Version) < 0)) { 325 | keepVersions.push(v.Version); 326 | toKeep--; 327 | } 328 | }); 329 | 330 | let deleted = 0; 331 | return BbPromise.map(versions.Versions, (v) => { 332 | if (keepVersions.indexOf( v.Version ) < 0) { 333 | S.utils.sDebug( `Deleting version ${v.Version} of ${func.name} function` ); 334 | deleted++; 335 | 336 | return persistentRequest( ()=> _this.aws.request('Lambda', 'deleteFunction', { 337 | FunctionName: lambdaName, 338 | Qualifier: v.Version 339 | }, _this.evt.options.stage, region) ); 340 | } 341 | else { 342 | S.utils.sDebug( `Keeping version ${v.Version} of ${func.name} function` ); 343 | } 344 | }, { concurrency: 3 }) 345 | .then(() => { 346 | return BbPromise.resolve({ 347 | lambdaName: lambdaName, 348 | functionName: func.name, 349 | deleted: deleted 350 | }); 351 | }); 352 | }) 353 | .then((result) => { 354 | 355 | // Add Function and Region 356 | if (!this.pruned) { 357 | this.pruned = {}; 358 | } 359 | if (!this.pruned[region]) { 360 | this.pruned[region] = []; 361 | } 362 | 363 | this.pruned[region].push(result); 364 | }); 365 | }); 366 | }; 367 | 368 | return BbPromise.map(_this.functions, pruneFunc, { concurrency: 3 }); 369 | } 370 | } 371 | 372 | return( AutoPrunePlugin ); 373 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-autoprune-plugin", 3 | "version": "0.2.1", 4 | "engines": { 5 | "node": ">=4.0" 6 | }, 7 | "description": "Serverless Auto-Prune Plugin - Delete old AWS Lambda versions", 8 | "author": "arabold", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/arabold/serverless-autoprune-plugin" 13 | }, 14 | "keywords": [ 15 | "serverless plugin sentry", 16 | "serverless framework plugin", 17 | "serverless applications", 18 | "serverless plugins", 19 | "api gateway", 20 | "lambda", 21 | "aws", 22 | "aws lambda", 23 | "amazon", 24 | "amazon web services", 25 | "serverless.com" 26 | ], 27 | "main": "index.js", 28 | "bin": {}, 29 | "devDependencies": {}, 30 | "dependencies": { 31 | "bluebird": "^3.3.5", 32 | "lodash": "^4.11.2" 33 | } 34 | } 35 | --------------------------------------------------------------------------------