The Serverless Framework has grown steadily over the past years, keeping lock step with the similarly named cloud paradigm. It’s now arguably the most popular framework for managing serverless applications and supports all major cloud providers. As a Serverless user, its flexibility can be both a blessing and a curse. There are many ways to configure your functions, define permissions, and generally structure your project. After getting it “wrong” many times, I’ve finally stumbled on some general guidelines I now follow in all of my Serverless projects, including team projects where I can. It may be a bit of extra work in the beginning, but I’ve found that following a few simple practices sets a straightforward pattern that’s easy to follow when a project grows beyond a few functions. These practices also help encapsulate concerns and create clear boundaries of work for team oriented projects.

Put your custom config in environment specific files

For any configuration that’s environment specific (dev, prod, etc.), create individual configuration files named after the stages you plan to deploy.

custom:

  config: ${file(config/${self:custom.stage, 'dev'}.yml)}

I’ve done environment specific configuration a few different ways but I’ve found this to be the most clear and most maintainable. It keeps your configuration definition easy to follow and simplifies the decision point to a single line. It also means that any custom config variables used in subsequent locations in the serverless.yml file don’t need to care about which stage is being deployed.

Set environment variables at the function level

Rather than set function environment variables at the provider level, set them on each individual function. This helps clarify which functions make use of which environment variables. It also allows you to reuse environment variable names while specifying different values in different function contexts. The exception to this rule would be environment variables that are reliably dependencies of all functions. For example, if you chose to set a logger level at the application level, it may be better to define a log level environment variable at the provider level.

Set IAM roles per function

If you’re using AWS, the `serverless-iam-roles-per-function` plugin provides the convenience of not specifying the serverless default log access policies for your functions, while still giving you the ability to define additional policy statements per function. The following example shows a function with the ability to publish to any IoT topic.

someFunction:

    handler: …

    events: …

    iamRoleStatements:

      – Effect: Allow

        Action:

          – iot:Publish

        Resource: “*”

 

Specifying roles per function helps define dependencies and actions of each function. It also follows the AWS best practice for least privileged access.

Split function definitions into individual files

Serverless files can grow to be quite large, even for smaller projects. I’ve found that I’m typically only concerned with one or two functions at a time when I’m looking to make a change, or find an entry point for tracing some code. Defining each function (including roles, and environment variables) in separate files helps to differentiate between individual function context, and the conceptual application level context. It might seem strange at first, but I’ve found this way of organizing functions to be much clearer to understand compared to a thousand line serverless.yml file.

There are an endless amount of factors that drive the layout and structure of any project, and it’s impossible to find a one-size-fits-all solution that works for every team. Each aspect of a project should be intentionally crafted, whether that means making use of some of the prior mentioned tactics, or developing your own standards for a navigable and maintainable layout.