How to Create a REST API with Azure Functions and the Serverless Framework - Part 2

Oct 10, 2019

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:

# Assumes you've already forked the repo
$ 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:

alt text

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.

Deploy your updates:
$ 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
        # Require an API Key if true
        subscriptionRequired: false
        displayName: Github API
        description: The GitHub API
        protocols:
          - https
        # Defaults to /api
        path: github
        # Azure resource tags
        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.

Deploy your updates:
$ 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

# .gitignore
...
# Webpack artifacts
.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.

alt text

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:

  1. Install dependencies
  2. Validate the changes (run quality gates)
  3. 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:

# Clean install
npm ci
# Runs tests and linting
npm test
# Serverless not contained within dev dependencies to avoid conflicts
# because most users have it installed globally on their dev machine
npm i serverless -g
# Deploy service
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
# pipelines/unit-tests.yml

# Only run on Pull Requests into `master` or `dev`
pr:
  branches:
    include:
    - master
    - dev

# Run pipeline on node 8 and 10 on Linux, Mac and Windows 
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

# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops#use-a-microsoft-hosted-agent
pool:
  vmImage: $(imageName)

steps:
- task: NodeTool@0
  inputs:
    versionSpec: $(node_version)
  displayName: 'Install Node.js'

# Make pipeline fail if tests or linting fail, linting occurs in `pretest` script
- bash: |
    set -euo pipefail
    npm ci
    npm test
  displayName: 'Run tests'
Deployment
# pipelines/deploy.yml

trigger:
  branches:
    include:
    - master

# https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&tabs=yaml
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'
    # Deploy service with prefix `gh`, stage `prod` and to region `West Europe`
  - bash: |
      npm ci
      sls deploy --prefix gh --stage prod --region "West Europe"
    env:
      # Azure Service Principal. Secrets need to be mapped here
      # USE THIS EXACT TEXT, DON'T COPY/PASTE YOUR CREDENTIALS HERE.
      # Azure DevOps will use the variables within
      # the variable group `sls-deploy-creds` to replace all the $() values
      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

A big part of our reason for investing time and effort into the serverless-azure-functions plugin was so that developers could easily deploy Azure Functions to solve more real-world, business-level scenarios. We hope that as you use the tool and discover areas for improvement that you'll file issues on the repo or even open up a pull request.

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.