A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins.
External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of NPM:
npm install --save custom-serverless-plugin
We need to tell Serverless that we want to use the plugin inside our service. We do this by adding the name of the Plugin to the plugins
section in the serverless.yml
file.
# serverless.yml file
plugins:
- custom-serverless-plugin
The plugins
section supports two formats:
Array object:
plugins:
- plugin1
- plugin2
Enhanced plugins object:
plugins:
localPath: './custom_serverless_plugins'
modules:
- plugin1
- plugin2
Plugins might want to add extra information which should be accessible to Serverless. The custom
section in the serverless.yml
file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
plugins:
- custom-serverless-plugin
custom:
customkey: customvalue
If you are working on a plugin or have a plugin that is just designed for one project, it can be loaded from the local .serverless_plugins
folder at the root of your service. Local plugins can be added in the plugins
array in serverless.yml
.
plugins:
- custom-serverless-plugin
Local plugins folder can be changed by enhancing plugins
object:
plugins:
localPath: './custom_serverless_plugins'
modules:
- custom-serverless-plugin
The custom-serverless-plugin
will be loaded from the custom_serverless_plugins
directory at the root of your service. If the localPath
is not provided or empty, the .serverless_plugins
directory will be used.
The plugin will be loaded based on being named custom-serverless-plugin.js
or custom-serverless-plugin\index.js
in the root of localPath
folder (.serverless_plugins
by default).
If you want to load a plugin from a specific directory without affecting other plugins, you can also specify a path relative to the root of your service:
plugins:
# This plugin will be loaded from the `.serverless_plugins/` or `node_modules/` directories
- custom-serverless-plugin
# This plugin will be loaded from the `sub/directory/` directory
- ./sub/directory/another-custom-plugin
Keep in mind that the order you define your plugins matters. When Serverless loads all the core plugins and then the custom plugins in the order you've defined them.
# serverless.yml
plugins:
- plugin1
- plugin2
In this case plugin1
is loaded before plugin2
.
Code which defines Commands, any Events within a Command, and any Hooks assigned to a Lifecycle Event.
- Command // CLI configuration, commands, subcommands, options
- LifecycleEvent(s) // Events that happen sequentially when the command is run
- Hook(s) // Code that runs when a Lifecycle Event happens during a Command
- LifecycleEvent(s) // Events that happen sequentially when the command is run
A CLI Command that can be called by a user, e.g. serverless deploy
. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the Lifecycle Events for the command. Every command defines its own lifecycle events.
'use strict';
class MyPlugin {
constructor() {
this.commands = {
deploy: {
lifecycleEvents: ['resources', 'functions'],
},
};
}
}
module.exports = MyPlugin;
Events that fire sequentially during a Command. The above example lists two Events. However, for each Event, an additional before
and after
event is created. Therefore, six Events exist in the above example:
before:deploy:resources
deploy:resources
after:deploy:resources
before:deploy:functions
deploy:functions
after:deploy:functions
The name of the command in front of lifecycle events when they are used for Hooks.
A Hook binds code to any lifecycle event from any command.
'use strict';
class Deploy {
constructor() {
this.commands = {
deploy: {
lifecycleEvents: ['resources', 'functions'],
},
};
this.hooks = {
'before:deploy:resources': this.beforeDeployResources,
'deploy:resources': this.deployResources,
'after:deploy:functions': this.afterDeployFunctions,
};
}
beforeDeployResources() {
console.log('Before Deploy Resources');
}
deployResources() {
console.log('Deploy Resources');
}
afterDeployFunctions() {
console.log('After Deploy Functions');
}
}
module.exports = Deploy;
As of version 1.52.0 of the Serverless framework, plugins can officially implement their own variable types for use in serverless config files.
Example:
'use strict';
class EchoTestVarPlugin {
constructor() {
getEchoTestValue(src) {
return src.slice(5);
}
getDependentEchoTestValue(src) {
return src.slice(5);
}
this.variableResolvers = {
echo: this.getEchoTestValue,
// if a variable type depends on profile/stage/region/credentials, to avoid infinite loops in
// trying to resolve variables that depend on themselves, specify as such by setting a
// dependendServiceName property on the variable getter
echoStageDependent: {
resolver: this.getDependentEchoTestValue,
serviceName: 'echo that isnt prepopulated',
isDisabledAtPrepopulation: true
};
}
}
The above plugin will add support for variables like ${echo:foobar}
and resolve to the key. EG:
${echo:foobar}
will resolve to 'foobar'
.
The data structure of this.variableResolvers
is an Object
with keys that are either a
function
or Object
.
The keys are used to generate the regex which matches the variable type. Eg, a key of test
will
match variables like ${test:foobar}
.
If the value is a function
it is used to resolve variables matched. It must be async
or return
a Promise
and accepts the variable string(with prefix but not the wrapping variable syntax,
eg test:foobar
) as it's only argument.
If the value is an Object
, it can have the following keys:
resolver
- required, a function, same requirements as described above.isDisabledAtPrepopulation
- optional, a boolean, disable this variable type when populating stage, region, and credentials. This is important for variable types that depend on AWS or other service that depend on those variablesserviceName
- required ifisDisabledAtPrepopulation === true
, a string to display to users if they try to use the variable type in one of the fields disabled for populating stage/region/credentials.
You can also nest commands, e.g. if you want to provide a command serverless deploy function
. Those nested commands have their own lifecycle events and do not inherit them from their parents.
'use strict';
class MyPlugin {
constructor() {
this.commands = {
deploy: {
lifecycleEvents: ['resources', 'functions'],
commands: {
function: {
lifecycleEvents: ['package', 'deploy'],
},
},
},
};
}
}
module.exports = MyPlugin;
Each (sub)command can have multiple Options.
Options are passed in with a double dash (--
) like this: serverless function deploy --function functionName
.
Option Shortcuts are passed in with a single dash (-
) like this: serverless function deploy -f functionName
.
The options
object will be passed in as the second parameter to the constructor of your plugin.
In it, you can optionally add a shortcut
property, as well as a required
property. The Framework will return an error if a required
Option is not included. You can also set a default
property if your option is not required.
Note: At this time, the Serverless Framework does not use parameters.
'use strict';
class Deploy {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
deploy: {
lifecycleEvents: ['functions'],
options: {
function: {
usage: 'Specify the function you want to deploy (e.g. "--function myFunction")',
shortcut: 'f',
required: true,
},
stage: {
usage: 'Specify the stage you want to deploy to. (e.g. "--stage prod")',
shortcut: 's',
default: 'dev',
},
},
},
};
this.hooks = {
'deploy:functions': this.deployFunction.bind(this),
};
}
deployFunction() {
console.log('Deploying function: ', this.options.function);
}
}
module.exports = Deploy;
Plugins can be provider specific which means that they are bound to a provider.
Note: Binding a plugin to a provider is optional. Serverless will always consider your plugin if you don't specify a provider
.
The provider definition should be added inside the plugins constructor:
'use strict';
class ProviderDeploy {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
// set the providers name here
this.provider = this.serverless.getProvider('providerName');
this.commands = {
deploy: {
lifecycleEvents: ['functions'],
options: {
function: {
usage: 'Specify the function you want to deploy (e.g. "--function myFunction")',
required: true,
},
},
},
};
this.hooks = {
'deploy:functions': this.deployFunction.bind(this),
};
}
deployFunction() {
console.log('Deploying function: ', this.options.function);
}
}
module.exports = ProviderDeploy;
The Plugin's functionality will now only be executed when the Serverless Service's provider matches the provider name which is defined inside the plugins constructor.
The serverless
instance which enables access to global service config during runtime is passed in as the first parameter to the plugin constructor.
'use strict';
class MyPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
log: {
lifecycleEvents: ['serverless'],
},
};
this.hooks = {
'log:serverless': this.logServerless.bind(this),
};
}
logServerless() {
console.log('Serverless instance: ', this.serverless);
}
}
module.exports = MyPlugin;
Note: Variable references in the serverless
instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your hooks.
Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command deploy
and an external command also wants to use deploy
) the Serverless CLI will print an error and exit. If you want to have your own deploy
command you need to name it something different like myCompanyDeploy
so they don't clash with existing plugins.
If your plugin adds support for additional params in serverless.yaml
file, you should also add validation rules to the Framework's schema. Otherwise, the Framework may place validation errors to command output about your params.
The Framework uses JSON-schema validation backed by AJV. You can extend initial schema inside your plugin constuctor by using defineCustomProperties
, defineCustomEvent
or defineProvider
helplers.
We'll walk though those heplers. You may also want to check out examples from helpers tests
Let's say your plugin depends on some properties defined in custom
section of serverless.yaml
file.
// serverless.yaml
custom:
yourPlugin:
someProperty: foobar
To add validation rules to these properties, your plugin would look like this:
class NewEventPlugin {
constructor(serverless) {
this.serverless = serverless;
// Create schema for your properties. For reference use https://github.com/ajv-validator/ajv
const newCustomPropSchema = {
type: 'object',
properties: {
someProperty: { type: 'string' },
},
required: ['someProperty'],
};
// Attach your piece of schema to main schema
serverless.configSchemaHandler.defineCustomProperties(newCustomPropSchema);
}
}
This way, if user sets someProperty
by mistake to false
, the Framework would display an error:
Serverless: Configuration error: custom.yourPlugin.someProperty should be string
Let's say your plugin adds support to a new yourPluginEvent
function event. To use this event, a user would need to have serverless.yaml
file like this:
// serverless.yaml
functions:
someFunc:
handler: handler.main
events:
- yourPluginEvent:
someProp: hello
anotherProp: 1
In this case your plugin should add validation rules inside your plugin constructor. Otherwise, the Framework would display an error message saying that your event is not supported:
Serverless: Configuration error: Unsupported function event 'yourPluginEvent'
To fix this error and more importantly to provide validation rules for your event, modify your plugin constructor with code like this:
class NewEventPlugin {
constructor(serverless) {
this.serverless = serverless;
// Create schema for your properties. For reference use https://github.com/ajv-validator/ajv
serverless.configSchemaHandler.defineFunctionEvent('providerName', 'yourPluginEvent', {
type: 'object',
properties: {
someProp: { type: 'string' },
anotherProp: { type: 'number' },
},
required: ['someProp'],
additionalProperties: false,
});
}
}
This way, if user sets anotherProp
by mistake to some-string
, the Framework would display an error:
Serverless: Configuration error: functions.someFunc.events[0].yourPluginEvent.anotherProp should be number
In case your plugin provides support for new provider, you would want to adjust validation schema. Here is example:
class NewProviderPlugin {
constructor(serverless) {
this.serverless = serverless;
// Create schema for your provider. For reference use https://github.com/ajv-validator/ajv
serverless.configSchemaHandler.defineProvider('newProvider', {
// Eventual reusable schema definitions (will be put to top level "definitions" object)
definitions: {
// ...
},
// Top level "provider" properties
provider: {
properties: {
stage: { type: 'string' },
remoteFunctionData: { type: 'null' },
},
},
// Function level properties
function: {
properties: { handler: { type: 'string' } },
},
// Function events definitions (can be defined here or via `defineFunctionEvent` helper)
functionEvents: {
someEvent: {
name: 'someEvent',
schema: {
type: 'object',
properties: {
someRequiredStringProp: { type: 'string' },
someNumberProp: { type: 'number' },
},
required: ['someRequiredStringProp'],
additionalProperties: false,
},
},
},
// Definition for eventual top level "resources" section
resources: {
type: 'object',
properties: {
// ...
},
},
});
}
}
The info
command which is used to display information about the deployment has detailed lifecycleEvents
you can hook into to add and display custom information.
Here's an example overview of the info lifecycle events the AWS implementation exposes:
-> info:info
-> aws:info:validate
-> aws:info:gatherData
-> aws:info:displayServiceInfo
-> aws:info:displayApiKeys
-> aws:info:displayEndpoints
-> aws:info:displayFunctions
-> aws:info:displayStackOutputs
Here you could e.g. hook into after:aws:info:gatherData
and implement your own data collection and display it to the user.
Note: Every provider implements its own info
plugin so you might want to take a look into the lifecycleEvents
the provider info
plugin exposes.