To make the most of this tutorial, sign up for Serverless Framework’s dashboard account for free: https://app.serverless.com

Overview

Now that you've created and deployed a basic API from Part 1, let's take a few more steps towards making that API more resilient and secure. This post will still be based on the example repo, and will follow the same "commit-per-step" format as Part 1, which contains Steps 1 and 2.

To pick up where we left off in the example repo (after having completed Step 2), run:

$ git clone https://github.com/<your-github-name>/sls-az-func-rest-api && git checkout cf46d1d

Step 3: Add unit testing and linting - (commit 465ecfe)

Because this isn't a blog post on unit tests, linting or quality gates in general, I'll just share the tools that I'm using and the quality gates that I added to the repository. Feel free to use them as stubs for your own future tests or lint rules.

For unit tests, I'm using the Jest test runner from Facebook. I've used it for several projects in the past and have never had any issues. Jest tests typically sit alongside the file they are testing, and end with .test.js . This is configurable within jest.config.js , which is found at the root of the project.

Because my code makes REST calls via axios , I'm using the axios-mock-adapter to mock the request & response. The tests that I wrote (issues.test.js and pulls.test.js) run some simple checks to make sure the correct URLs are hit and return the expected responses.

For linting, I'm using ESLint with a very basic configuration, found in .eslintrc.json . To run a lint check, you can run:

$ npm run lint

Many errors can be fixed automatically with:

$ npm run lint:fix

Run your tests with:

$ npm test

For more details, take a look at the commit in the example repo or check out the commit locally

$ git checkout 465ecfe

Step 4: Add basic API Management Configuration - (commit c593308)

This was one of the first features we implemented into the v1 of the serverless-azure-functions plugin. because most Azure Function Apps are REST APIs, and it's hard to have a real-world API in Azure without API Management.

If you have no special requirements for API Management, the plugin will actually generate the default configuration for you if you just include:

... provider: ... apim: true

That's exactly what I did for Step 4. Also, because we want API Management to be the only entry point for our API endpoints, I also changed each function's authLevel to function . This requires a function-specific API key for authentication. You can see in the screenshot what happens in the first command, when I try to curl the original function URL. I get a 401 response code. But when I hit the URL provided by API Management, I get the response I expect:

For more details on authLevel , check out the trigger configuration docs.

Consumption SKU

One important thing to note is that the API Management configuration will default to the consumption SKU, which recently went GA. For now, the only regions where Consumption API Management is allowed are:

North Central US

West US

West Europe

North Europe

Southeast Asia

Australia East

If you are deploying to a region outside of that list, you will need to specify a different SKU ( Developer , Basic , Standard or Premium ) within the apim configuration, which will be demonstrated in the next section.

$ sls deploy

Step 5: Add more advanced API Management Configuration - (commit 38413a0)

If you need a few more knobs to turn when configuring your API Management instance, you can provide a more verbose configuration. Here is the verbose config I added to the sample repo (the ... means the rest of the config for that section stayed the same):

service: sls-az-func-rest-api provider: ... apim: apis: - name: github-api subscriptionRequired: false displayName: Github API description: The GitHub API protocols: - https path: github tags: - apimTag1 - apimTag2 authorization: none backends: - name: github-backend url: api/github cors: allowCredentials: false allowedOrigins: - "*" allowedMethods: - GET - POST - PUT - DELETE - PATCH allowedHeaders: - "*" exposeHeaders: - "*" ... functions: issues: ... apim: api: github-api backend: github-backend operations: - method: get urlTemplate: /issues displayName: GetIssues pulls: ... apim: api: github-api backend: github-backend operations: - method: get urlTemplate: /pulls displayName: GetPullRequests

If you did not want the Consumption SKU of API Management, you would need to have a verbose configuration and specify the sku as:

provider: ... apim: ... sku: name: { Consumption|Developer|Basic|Standard|Premium }

The example just uses the default and deploys to region(s) where Consumption API Management is currently available.

$ sls deploy

(Optional) Step 5.1: Revert back to basic API Management configuration - (commit 4c5803f)

To make the demo simple and easy to follow, I'm going to revert my apim configuration back to the defaults:

apim: true

You might be able to do the same, depending on your requirements.

Step 6: Add Webpack configuration - (commit 1aefac7)

Webpack dramatically reduces the packaging time as well as the size of your deployed package. After making these changes, your packaged Function App will be optimized with Webpack (You can run sls package to package it up or just run sls deploy which will include packaging as part of the lifecycle).

Just as an example, even for this very small application, my package size went from 324 KB to 28 KB.

To accomplish this, we'll use another awesome Serverless plugin, serverless-webpack to make Webpacking our Azure Function app really easy.

First thing you'll want to do, assuming you're working through this tutorial in your own git repository, is add the generated Webpack folder to your .gitignore

... .webpack/

Next, we'll need to install 3 packages from npm:

$ npm i serverless-webpack webpack webpack-cli --save-dev

Then we'll add the plugin to our serverless.yml :

plugins: - serverless-azure-functions - serverless-webpack

And then copy this exact code into webpack.config.js in the root of your service directory:

const path = require ( "path" ); const slsw = require ( "serverless-webpack" ); module .exports = { entry : slsw.lib.entries, target : "node" , output : { libraryTarget : "commonjs2" , library : "index" , path : path.resolve(__dirname, ".webpack" ), filename : "[name].js" }, plugins : [], };

And just like that, your deployed Azure Function apps will be webpacked and ready to go.

Step 7: Enable Serverless CLI configuration - (commit 4cb42fd)

If you're running a real-life production service, you will most likely be deploying to multiple regions and multiple stages. Maybe merges to your dev branch will trigger deployments to your dev environment, master into prod , etc. I'll show you an example of that in Step 8. To accomplish CLI-level configurability, we need to make a few changes serverless.yml .

provider: region: ${opt:region, 'West US' } stage: ${opt:stage, 'dev' } prefix: ${opt:prefix, 'demo' }

As you might have guessed, the values West US , dev and demo are my default values. If I wanted to deploy my service to North Central US and West Europe , but keep everything else the same, I would run:

$ sls deploy --region "North Central US" $ sls deploy --region "West Europe"

We could do similar operations with --prefix and --stage . Now let's create a pipeline that actually does this.

Step 8: Add CI/CD (with Azure DevOps) - (commit a8fabf6)

For the CI/CD on my sample repo, I'm using Azure DevOps, but it would work the same on any other service you want to use. If you want to use Azure DevOps for an open-source project, here are a few steps to get started

No matter the CI/CD environment, here is what we are looking to accomplish:

Install dependencies Validate the changes (run quality gates) Deploy the service

These steps can all be accomplished in just a few CLI commands. At bare minimum, we'll want to run something like:

npm ci npm test npm i serverless -g sls deploy

There are a lot more bells and whistles we could add, but that's essentially what it boils down to. Of course, we'll need authentication in whatever system we're deploying from, and that's where the service principal will come in. I'll show you how to use the service principal in the deploy.yml pipeline below.

For my pipelines, I'm actually going to split up my CI and CD into unit-tests.yml and deploy.yml . Unit tests will be run on PRs into master or dev (this is assuming there are branch policies in place to prevent devs from pushing straight to either branch). Deployment will be run on commits (merges) to master .

Unit Tests

pr: branches: include: - master - dev strategy: matrix: Linux_Node8: imageName: 'ubuntu-16.04' node_version: 8. x Linux_Node10: imageName: 'ubuntu-16.04' node_version: 10. x Mac_Node8: imageName: 'macos-10.14' node_version: 8. x Mac_Node10: imageName: 'macos-10.14' node_version: 10. x Windows_Node8: imageName: 'win1803' node_version: 8. x Windows_Node10: imageName: 'win1803' node_version: 10. x pool: vmImage: $(imageName) steps: - task: NodeTool@0 inputs: versionSpec: $(node_version) displayName: 'Install Node.js' - bash: | set -euo pipefail npm ci npm test displayName: 'Run tests'

Deployment

trigger: branches: include: - master variables: - group: sls-deploy-creds jobs: - job: "Deploy_Azure_Function_App" timeoutInMinutes: 30 cancelTimeoutInMinutes: 1 pool: vmImage: 'ubuntu-16.04' steps: - task: NodeTool@0 inputs: versionSpec: 10. x displayName: 'Install Node.js' - bash: | npm install -g serverless displayName: 'Install Serverless' - bash: | npm ci sls deploy --prefix gh --stage prod --region "West Europe" env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_TENANT_ID: $(AZURE_TENANT_ID) AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET) displayName: 'Deploy Azure Function App'

Notice this line in the deployment pipeline that leverages our setup from Step 7. You might have multiple pipelines for the different stages, you might dynamically infer these values from the branch name or you might just provide the values as environment variables. The point of the setup in Step 7 was to provide you the flexibility to deploy your service to wherever you see fit at the time, without needing to change your serverless.yml file.

Concluding Thoughts