loading...
Cover image for How to write and test a serverless plugin

How to write and test a serverless plugin

dvddpl profile image Davide de Paolis ・6 min read

If you are building serverless applications, and more precisely if you are building applications with the Serverless framework you will inevitably use some plugins, which are nothing more than javascript code that extends, overwrites or add new functionalities to the framework.

Some of the plugins that we often use are for example Offline which allow to emulate Lambda and API Gateway for local development, or the WarmUp which "solves" the problem of Lambda coldstarts or Split Stacks which migrates Cloud Formation resources to nested stack to work around CF resource limit.

When we started using AWS and Serverless I was looking at plugins like something whose code must have been very difficult or complex.

they are "hacking" the framework core!!

plugins are amazing

Actually, when you look at the source code you see that the implementation is often very very simple (the scope of a single plugin should be very limited - therefore mostly it is just a couple of methods using some code from AWS SDK ( or other providers´)

There are already thousands of community-contributed plugins available, so often is just a matter of realizing what you need in your project and googling for the right plugin but sometimes you might want to experiment or add some custom functionality specific to your company or project.

Recently we realized that a "simple shell script" that we wrote to automate some tasks after sls deploy was completed, grew - as often happens - to a hundred lines.
Therefore we decided to give custom plugins a go.

If this is your case a very good start are the articles on the serverless website:

So I will not bore you with another basic tutorial but rather share some small aspects which at first slowed me down in the development of our first plugin.

How to run and test a plugin that I am developing?

To use a serverless plugin you just run npm install to add it to your package.json dependencies and then add it to the ´plugins´ section of your serverless.yml file

To use a plugin that has not yet been published you can specify the folder where you have it:

plugins
 - myplugins/my-first-awesome-serverless-plugin

or

plugins
  - ./my-first-awesome-serverless-plugin

Just be aware that passing a local path outside your root will not work though

plugins
  - ../../my-first-awesome-serverless-plugin  #this won't work

If you don´t intend ever to publish it and it will always reside alongside your project codebase then just put it into the suggested default folder .serverless_plugins

and import it normally as if it was coming from a published package

plugins
 - my-first-awesome-serverless-plugin

If on the other hand you know that your plugin will be reused and you want to publish it to npm ( either public or - like in our case - under a scoped private registry) you can keep it in a different folder outside and either use npm link(even though I always found npm link a bit cumbersome).

The advantage is that you can install the package as usual and nothing will change when you release the plugin and commit your serverless project

plugins
 - my-first-awesome-serverless-plugin

Another option is just installing the package from a local directory

npm install -D ../../my-first-awesome-serverless-plugin

This will add the dependency to your package.json and create a symlink to your folder (but you have to remember to change the dependency in the package json to the real publish one as soon as you have publish it.

One last thing, in case you have a scoped package just remember that you need to use quotes to import the plugin in your yml, otherwise you will get a formatting exception:

  - "@yourscope/your-plugin"

Lifecycle Events, Hooks and Commands

A Command is the functionality you want to add to your plugin.

Lifecycle events are fired sequentially during a Command. They are basically the logical steps in its process/functionality.

Hooks bind your code to that specific stage of the process.

You can define your commands in the constructor of your plugin:

 this.commands = {
            setDomainName: {
                usage: "Sets Hosted UI Domain Name used by Cognito User Pool App Integration",
                lifecycleEvents: ['set'],
                options: {
                    domainName: {
                        usage:
                        'Specify the domain name you want to set ' +
                        '(e.g. "--domain-name \'my-app\'" or "-dn \'my-app\'")',
                        required: true,
                        shortcut: 'dn',
                    },
                },
            },
        };

And you can specify the code that must be executed in the hooks section of the constructor:

        this.hooks = {
            'setDomainName:set':  this.addDomainName.bind(this)
        };

remember to compose CommandName:LifecycleEvent when creating the key name in your hooks

Your command will be shown when you run sls help and you can invoke it simply by sls yourCommand --yourparams

If you don´t need your own command but you´d just rather override or add some functionality on existing lifecycle events, just specify the hook section binding your code to the event:

 this.hooks = {
      'before:package:finalize':  this.myCustomCode.bind(this)
    }

Plugin Context

The plugin always receives in the constructor options and serverless params. serverless being the global service configuration.

At the beginning I found very useful logging the entire serverless param to understand all the props I had access to:

console.log('Serverless instance: ', this.serverless);
// use the normal console  otherwise you will get an error
this.serverless.cli.log(JSON.stringify(this.serverless), 'Serverless instance: ') // Error: Converting circular structure to JSON

And what you have access to is basically everything that you have in your serverless yml file. You just need to figure out the nesting and the prop names:

this.serverless.service.provider.name

this.serverless.service.resources.Resources.IdentityPool.Properties.IdentityPoolName

You can find more info here

Something that you often see in many plugins´ source code and it's definitely something you will need in yours is the stack name:

get stackName() {
    return `${this.serverless.service.service}-${this.options.stage}`;
  }

and expecially if - like in our case - you have to act on deployed resources you want to know all the resources in your CloudFormation stack:

async retrieveOutputs() {
        return this.serverless.getProvider('aws').request(
            'CloudFormation',
            'describeStacks',
            {StackName: this.stackName},
            this.serverless.getProvider('aws').getStage(),
            this.serverless.getProvider('aws').getRegion()
        ).then(described=> described.Stacks[0].Outputs)
    }

Just keep in mind that this will work only after the stack has been deployed, so you can use it in a command - but if it´s a command meant to be run standalone - it will work only if your stack has been deployed at least once.

Where to go from here

Define your main method which is bound to a hook and write whatever you need - normally just a bunch of commands interacting with AWS SDK.
The content of your plugin can be really everything, and it really depends on what you need and what you want to achieve.

As usual, I suggest - to avoid reinventing the wheel - to search for something already available and read the source code.

Read the source

It might be exactly what you are looking for, like for example
a plugin to empty S3 Buckets before the stack is removed or a plugin to notify on Slack that the deployment is completed or it could serve anyway as a starting point ( and a learning base ) for what you want to implement.

I hope it helps.

Discussion

pic
Editor guide
Collapse
unfor19 profile image
Meir Gabay

Exactly what I've been looking for, thanks!