So you already know the Serverless Framework - it’s handy for converting a pretty simple config format into a pretty complex CloudFormation template. When you’re deploying, it builds that template and uploads your code for you. But if this isn’t your first serverless application, you might have special requirements. I’ve had needs like custom metrics, performing extra security/preflight checks, or removing the auto-created resources (such as the Cognito User Pool) from the template and subbing in my own.
To do that, I needed a way to make sure before every deploy I could make changes to the template before it was sent to AWS. Fortunately, the Serverless Framework gives us tons of customization options. Under the hood all the framework features are implemented with the same plugin interface we’re about to use, meaning we get 100% of the power core developers do.
Tiny Project
Before we write a plugin, we have to have a project to test it on. So here’s a bare-bones config and Lambda function we can use:
service: plugin-test-drive
frameworkVersion: ">=1.16 <=1.17"
provider:
name: aws
runtime: 'python3.6'
versionFunctions: false
functions:
hello:
handler: handler.hello
events:
- http:
path: /hello
method: get
world:
handler: handler.world
events:
- http:
path: /world
method: get
And now for the contents of handler.py
:
def hello(event, context):
return {
"statusCode": 200,
"body": "HELLO!"
}
def world(event, context):
return {
"statusCode": 200,
"body": "WORLD!"
}
This project has exactly one job: respond to HTTP requests so that we have functions to play with in our plugin. So now we need to talk about what our plugin is actually for.
Goals
To have something for our plugin to do, let’s pick a simple enhancement to what Serverless is already doing. We’ll be extending the template generated in the framework by adding a retention setting to all our function logs. This will mean that after some number of days, all the log data will be deleted, which is handy for staging environments where logs quickly become useless as the codebase changes and for saving money on storage.
Right now, if we deploy our project the template will have lines like this:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "The AWS CloudFormation template for this Serverless application",
"Resources": {
...
"HelloLogGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/aws/lambda/plugin-test-drive-dev-hello"
}
},
"WorldLogGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/aws/lambda/plugin-test-drive-dev-world"
}
},
...
},
"Outputs": {...}
}
To set the retention lifecycle, we need to add a RetentionInDays
option to
the AWS::Logs::LogGroup resources in the template, so they look more like this.
"HelloLogGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/aws/lambda/plugin-test-drive-dev-hello",
"RetentionInDays": 3
}
}
To do that, we need to get our custom code into the execution path of the
framework so we can edit the CloudFormation template before the Serverless
Framework uploads it during the serverless deploy
command.
Commands, Hooks, and Lifecycle Events
Under the hood, it’s important to know that everything in the Serverless
Framework is implemented as commands (such as serverless deploy
) that trigger
hooks and lifecycle events. A lifecycle event represents a part of what a
command is built to do. To see how this works, we’ll take examples right from
the framework’s codebase. The package
command has loads of events, so we’ll
use those. In package.js you can see a list of events defined:
initialize
setupProviderConfiguration
createDeploymentArtifacts
compileFunctions
compileEvents
finalize
cleanup
Each of these events can be thought of as a step in the process of packaging
your code to get it ready, and each of these lifecycle events gets its own set
of hooks. A hook lets you execute your own custom code at that point in the
process. Each lifecycle event has a before:
and after:
hook that you can
use to run code right before an event fires, or right after.
Hooks have predictable names that follow the format
[time]:[command]:[lifecycle event]
so to do something right before the events
are compiled, you’d use the hook before:package:compileEvents
.
For our purposes, we know we have to get the CloudFormation template after it’s
done being generated, but before it’s sent off to AWS. To do that, we’ll use
the hook before:package:finalize
so the functions and their events are all
included and we can change any resource we like.
Changing CloudFormation Resources
So now we know what we have to do - hang our code on the
before:package:finalize
hook and then make the right changes to the
AWS::Logs::LogGroup
resources in the template. The template is stored on the
serverless
object that gets passed to templates as a map/object, so we can
just use its properties to find and change what we want.
'use strict'
class SetCycle {
constructor (serverless, options) {
this.hooks = {
// this is where we declare the hook we want our code to run
'before:package:finalize': function () { resetLogLifetime(serverless) }
}
}
}
function resetLogLifetime (serverless) {
// shorten the name of the "Resources" section of the template
let rsrc = serverless.service.provider.compiledCloudFormationTemplate.Resources
for (let key in rsrc) {
if (rsrc[key].Type === 'AWS::Logs::LogGroup') {
// this is the real feature: where we change all the LogGroup resources
rsrc[key].Properties.RetentionInDays = 3
}
}
}
// now we need to make our plugin object available to the framework to execute
module.exports = SetCycle
And that’s all! Just 20 lines of code to get the right info and change the parameter we want. The key here is to properly implement the constructor that the Serverless Framework expects of plugins, otherwise we won’t be able to get the data we need.
After we’ve changed the template object, the finalize
step of the
serverless package
command will take the now-modified template and write it
to the .serverless/cloudformation-template-update-stack.json
in our project
directory.
Enabling the Plugin
Before our changes show up, you need to make a .serverless_plugins
directory
in your project, and copy the JavaScript code above into a file called
.serverless_plugins/set-log-lifecycle.js
. This method is for plugins that are
local to your project, there’s a second method that involves creating a full
NPM module but I won’t be covering that in this article.
Once your plugin is saved, we need to enable the plugin in your project. In
your serverless.yml
, we’ll add a plugins:
section:
service: plugin-test-drive
frameworkVersion: ">=1.16 <=1.17"
# the new lines:
plugins:
- set-log-lifecycle
With this change, you can run serverless deploy
and then open the
CloudFormation template generated, and you’ll see the log groups have changed:
"HelloLogGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/aws/lambda/plugin-test-drive-dev-hello",
"RetentionInDays": 3
}
}
You can even confirm this in the web console by navigating to the CloudWatch
Logs dashboard and looking for the “Expire Events After” column to see if all
the plugin-test-drive-dev
functions have the right expiration policy on them.
Congratulations, you’ve just implemented your first plugin for the Serverless Framework, now you can extend the CloudFormation template or replace Framework resources any time you like.
Wrapping Up
In this post, we’ve covered the basics of writing plugins, but there’s so much more! Learn about the plugin hooks and more here.
Keep up with future posts via RSS. If you have suggestions, questions, or comments feel free to email me, ryan@serverlesscode.com .