Customizing the Serverless Framework With Plugins

Sometimes You Need More Than What's in the Box

Posted by Ryan S. Brown on Tue, Jun 27, 2017
In Mini-Project
Tags: lambda, serverless, serverless framework, monitoring

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.

AWS CloudWatch Logs Console

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 .


Tweet this, send to Hackernews, or post on Reddit