The Serverless plugin System is at the core of the Serverless framework.
The main goals of the plugin system are:
- Separation of CLI configuration and plugin logic
- We want any plugin author to be able to easily create new commands within the Serverless framework and extend existing commands. To achieve this we've created a strong separation between CLI and plugins that has clear interfaces between each other.
- Separation between logic of different plugins
- Different plugins need to have an easy way to run independently and after each other without defining dependencies between each other.
- Greater Extensibility
- Plugins need to be able to easily integrate into the lifecycle of a command independent of other Plugins that are running and can extend the functionality of Serverless easily.
A plugin encapsulates commands (and corresponding lifecycle events) and hooks in a shareable way. A plugin is not forced to provide both, they can only consist of a list of commands and lifecycle events or only of hooks.
A command represents a CLI command that can be called by a user, e.g. serverless deploy
would be the deploy
command.
A command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the lifecycle events for this particular command. Every command defines its own lifecycle events, so different commands can have completely different lifecycles.
The commands that come with Serverless (e.g. deploy
, remove
, …) are implemented in the exact same way as commands built by other users. This means that lifecycle events we define for our commands do not have any special meaning in Serverless or for other plugins. Every command is free to have its own lifecycle events, none of them are more special than others.
'use strict';
class MyPlugin {
constructor() {
this.commands = {
deploy: {
lifecycleEvents: [
'resources',
'functions'
]
},
};
}
}
module.exports = MyPlugin;
We automatically put the name of the command in front of lifecycle events when they are used for hooks. So in a hook the following syntax needs to be used.
CommandName:LifecycleEventName
Which would be deploy:resources, and deploy:functions in a hook definition (which we will show in more detail below).
In addition to the lifecycle events defined here we will create 2 additional events for each:
before:CommandName:LifecycleEventName
after:CommandName:LifecycleEventName
Following the above example we’ll have these lifecycle events for our myPlugin
plugin:
For the deploy:resources lifecycleEvent:
before:deploy:resources
deploy:resources
after:deploy:resources
For the deploy:functions lifecycleEvent:
before:deploy:functions
deploy:functions
after:deploy:functions
These names will be used as hooks to include plugin logic. This allows to set up lifecycle events with generic names, but still make sure they are only executed for specific commands.
You can also nest commands, e.g. if you want to provide a command serverless deploy single
you can simply nest commands as defined in the following example. 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: {
single: {
lifecycleEvents: [
'resources',
'functions'
],
},
},
},
}
}
}
module.exports = MyPlugin;
Hooks allow to connect specific lifecycle events to functions in a plugin. In the constructor of your class you define a hooks
variable that the Plugin System will use once a specific command is running. Any hook can bind to any lifecycle event from any command, not just from commands that the same plugin provides.
This allows to extend any command with additional functionality.
'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;
The serverless
instance which enables access to the whole Serverless setup 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;
Each (sub)command can have multiple options (and corresponding shortcuts if available).
Options are passed in with a double dash (--
) like this: serverless function deploy --function functionName
.
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.
'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")'
}
}
},
};
this.hooks = {
'deploy:functions': this.deployFunction.bind(this)
}
}
deployFunction() {
console.log('Deploying function: ', this.options.function);
}
}
module.exports = Deploy;
Options can be marked as required. This way the plugin manager will automatically raise an error if a required option
is not passed in via the CLI. You can mark options as required with the help of required: true
inside the options
definition.
'use strict';
class Deploy {
constructor(serverless, options) {
this.options = options;
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 = Deploy;
Options can also provide shortcuts. Shortcuts make it more convenient to enter long commands. Serverless will translate shortcuts into options under the hood which means that the option the shortcut belongs to will be replaced with the value of the shortcut (if the shortcut is given).
You can define shortcuts by setting the shortcut
property in the options definition.
Note: A shortcut should be unique inside of a plugin.
'use strict';
class Deploy {
constructor(serverless, options) {
this.options = options;
this.commands = {
deploy: {
lifecycleEvents: [
'functions'
],
options: {
function: {
usage: 'Specify the function you want to deploy (e.g. "--function myFunction" or "-f myFunction")',
required: true,
shortcut: 'f'
}
}
},
};
this.hooks = {
'deploy:functions': this.deployFunction.bind(this)
}
}
deployFunction() {
console.log('Deploying function: ', this.options.function);
}
}
module.exports = Deploy;
You can provide a custom validation rule in form of a regular expression for your option. Serverless will raise an error you can specify if the input for the option does not match the regular expression.
In the following example we'll validate the retries
option against a regular expression that checks if the value is a number.
'use strict';
class Deploy {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
deploy: {
lifecycleEvents: [
'functions'
],
options: {
retries: {
usage: 'Number of retries which should be performed',
customValidation: {
regularExpression: /^[0-9]+$/,
errorMessage: 'Retries should be a number',
},
}
}
},
};
this.hooks = {
'deploy:functions': this.deployFunction.bind(this)
}
}
deployFunction() {
console.log(`Deploying function… Will retry ${this.options.retries} times`);
}
}
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 and can be the name of the provider (as a string) or the provider plugin instance.
The string representation simply tells Serverless that this plugin should only be loaded if the provider defined in the service matches the one of the plugin.
'use strict';
class ProviderDeploy {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
// set the providers name here
this.provider = '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;
If you want to get access to provider specific utilities such as the SDK you can get the provider plugin instance with the help of the
this.serverless.getProvider('providerName')
function and set it to the plugins provider
property.
You can access the wrapped methods via this.provider.<methodName>
.
The usage of the plugins provider
property will load the plugin only if the provider
matches the one defined in the service.
'use strict';
class ProviderInvoke {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
// get access to the provider plugin instance here
this.provider = this.serverless.getProvider('providerName');
this.commands = {
invoke: {
lifecycleEvents: [
'function'
],
options: {
function: {
usage: 'Specify the function you want to invoke (e.g. "--function myFunction")',
required: true
}
}
},
};
this.hooks = {
'invoke:function': this.invokeFunction.bind(this)
}
}
invokeFunction() {
// use the "request" method from the provider plugin
return this.provider.request('function', 'invoke', this.options.function);
}
}
module.exports = ProviderInvoke;
A user has to define the plugins they want to use in the root level of the
serverless.yml
file:
plugins:
- custom-plugin-1
- custom-plugin-2
We do not auto-detect plugins from installed dependencies so users do not run into any surprises and we cut down on the startup time of the tool. Through JSON-REF users can share configuration for used plugins between
serverless.yml
files in one repository.
After the serverless.yml
configuration file is loaded the plugin system will load all the commands and plugins and initialize the plugin system.
Sometimes your plugin needs to setup some custom options. The serverless.yml
file provides the custom
section where you can add options your plugin can use.
plugins:
- my-greet-plugin
custom:
greeting: hello
Plugins are registered in the order they are defined through our system and the
serverless.yml
file. By default we will load the
core plugins first, then we will load all plugins according to the order given in the
serverless.yml
file.
This means the Serverless core plugins will always be executed first for every lifecycle event before 3rd party plugins. If external plugins should be running before our plugins they should generally be able to hook into an earlier lifecycle event.
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. Commands need to be unique in the current service context they are executed. So 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.