Overview
With the recent updates to the serverless-azure-functions
plugin, it is now easier than ever to create, deploy and maintain a real-world REST API running on Azure Functions. This post will walk you through the first few steps of doing that.
To see the full end-to-end example used to create this demo, check out my GitHub repo. I structured each commit to follow the steps described in this post. Any steps named Step X.X
are steps that involve no code or configuration changes (and thus not tracked by source control), but actions that could/should be taken at that point in the process. This is done to preserve the "commit-per-step" structure of the example repo.
This post will only cover the basics of creating and deploying a REST API with Azure Functions, which includes step 1 and step 2 from the example repo. Stay tuned for posts on the additional steps in the future.
I will make the assumption that you have the Serverless Framework installed globally. If you do not (or have not updated in a while), run:
npm i serverless -g
Also, the serverless
CLI can be referenced by either serverless
or sls
. I will use sls
in this post just because it's shorter, but serverless
would work just the same.
Step 1: Create your local Azure Function project
Let's begin by creating our Azure Function project with a template from serverless.
sls create -t azure-nodejs -p sls-az-func-rest-api
The resulting project will be in the directory sls-az-func-rest-api
. cd
into that directory and run npm install
. To make sure you have the latest version of the Azure Functions plugin, run:
npm install serverless-azure-functions --save
It’s important to note that the generated serverless.yml
file will contain a lot of commented lines, which start with #
. Those are purely for your benefit in exploring features of the Azure Functions plugin, and can be safely removed.
Step 2: Add your own handlers
For the sake of this demo, we’re going to create a basic wrapper of the GitHub API for issues and pull requests.
As you’ve probably already noticed, the azure-nodejs
template comes preloaded with two functions: hello
and goodbye
. Let’s remove those before we start adding our own code. To do this, remove both the hello.js
and goodbye.js
files. Also, remove their configuration definitions from serverless.yml
.
Right now your file structure should look something like:
sls-az-func-rest-api
|-- host.json
|-- package.json
|-- README.md
|-- serverless.yml
and your serverless.yml
should look like (not including any comments):
service: sls-az-func-rest-api
provider:
name: azure
location: East US
runtime: nodejs10.x
plugins:
- serverless-azure-functions
package:
exclude:
- local.settings.json
- .vscode/**
functions:
Add Code
Let’s add in our own code. We’ll start by creating the directory src/handlers
. This, perhaps to your great surprise, will be where our handlers will live. Inside that directory, we will put our two handlers: issues.js and pulls.js.
// src/handlers/issues.js
const utils = require("../utils");
const axios = require("axios");
module.exports.handler = async (context, req) => {
context.log("Issue Handler hit");
const owner = utils.getQueryOrBodyParam(req, "owner");
const repo = utils.getQueryOrBodyParam(req, "repo");
if (owner && repo) {
const response = await axios({
url: `https://api.github.com/repos/${owner}/${repo}/issues`,
method: "get"
});
context.res = {
status: 200,
body: response.data
};
} else {
context.res = {
status: 400,
body: "Please pass the name of an owner and a repo in the request"
};
}
};
// src/handlers/pulls.js
const utils = require("../utils");
const axios = require("axios");
module.exports.handler = async (context, req) => {
context.log("Pull Request Handler hit");
const owner = utils.getQueryOrBodyParam(req, "owner");
const repo = utils.getQueryOrBodyParam(req, "repo");
if (owner && repo) {
const response = await axios({
url: `https://api.github.com/repos/${owner}/${repo}/pulls`,
method: "get"
});
context.res = {
status: 200,
body: response.data
};
} else {
context.res = {
status: 400,
body: "Please pass the name of an owner and a repo in the request"
};
}
};
Just for fun, we’ll also add a utils.js file for shared utility functions across handlers, and we’ll put that just inside the src
directory.
// src/utils.js
/** Gets the param from either the query string
* or body of request
*/
module.exports.getQueryOrBodyParam = (req, param) => {
const { query, body } = req;
if (query && query[param]) {
return query[param];
}
if (body && body[param]) {
return body[param];
}
};
You’ll also note that the handlers are using a popular NPM package for HTTP requests, axios
. Run npm install axios --save
in your service root directory.
Current Folder structure
sls-az-func-rest-api
|-- src
|-- handlers
|-- issues.js
|-- pulls.js
|-- utils.js
|-- host.json
|-- package.json
|-- README.md
|-- serverless.yml
Now we need to add our new handlers to the serverless configuration, which will now look like:
service: sls-az-func-rest-api
provider:
name: azure
location: East US
runtime: nodejs10.x
plugins:
- serverless-azure-functions
package:
exclude:
- local.settings.json
- .vscode/**
functions:
issues:
handler: src/handlers/issues.handler
events:
- http: true
x-azure-settings:
authLevel: anonymous
pulls:
handler: src/handlers/pulls.handler
events:
- http: true
x-azure-settings:
authLevel: anonymous
Step 2.1: Test your API Locally
Run the following command in your project directory to test your local service.
sls offline
This will generate a directory for each of your functions with the file function.json
in each of those directories. This file contains metadata for the “bindings” of the Azure function, and will be cleaned up when you stop the process. You shouldn’t try to change the bindings files yourself, as they will be cleaned up and regenerated from serverless.yml
. If you make changes to your serverless.yml
file, you’ll need to exit the process and restart. Changes to code, however, will trigger a hot reload and won’t require a restart.
Here is what you can expect as output when you run sls offline
:
When you see the “Http Functions” in the log, you are good to invoke your local service.
One easy way to test your functions is to start up the offline process in one terminal, and then in another terminal, run:
sls invoke local -f {functionName} -p {fileContainingTestData.json}
Let’s create a file with some sample data at the root of our project, and we’ll just call it data.json
:
{
"owner": "serverless",
"repo": "serverless-azure-functions"
}
Luckily, owner
and repo
are the same parameters expected by both the issues
and pulls
handlers, so we can use this file to test both.
We’ll keep our offline
process running in one terminal. I’ll open up another (pro tip: use the “Split Terminal” in the VS Code integrated terminal), and run:
sls invoke local -f pulls -p data.json
Here’s my output:
You can see that it made a GET
request to the locally hosted API and added the info from data.json
as query parameters. There are no restrictions on HTTP methods, you would just need to specify in the CLI if it’s not a GET
. (Example: sls invoke local -f pulls -p data.json -m POST
)
You could also run a simple curl
command that would accomplish the same thing:
And here is the output in the terminal running the API. You can see our console.log
statement from the handler output here:
When I’m done running the service locally, I’ll hit Ctrl/Cmd + C
in the API terminal to stop the process. You can see that it cleans up those metadata files we discussed earlier:
Step 2.2: Deploy
Authentication
That’s all the configuration we need, so we’re ready to deploy this Function App. In order to deploy, we’ll need to authenticate with Azure. There are two options for authentication: interactive login and a service principal (which, if you are unfamiliar, is essentially a service account).
At first, when you run a command that requires authentication, the Interactive Login will open up a webpage for you to enter a code. You’ll only need to do this once. The authentication results are cached to your local machine.
If you have a service principal, you’ll set the appropriate environment variables on your machine, and the plugin will skip the interactive login process. Unfortunately, if you’re using a free trial account, your only option is a service principal. The process for creating one and setting up your environment variables is detailed in the Azure plugin README.
Deploy Command
With configuration and authentication in place, let’s ship this thing. From the root of your project directory, run:
sls deploy
and watch the magic happen. Your app will be packaged up into a .zip
file, which will be located in the .serverless
directory at the root of your project. From there, an Azure resource group will be created for your application, containing things like your storage account, Function App, and more. After the resource group is created, the zipped code will be deployed to your newly created function app and the URLs for your functions will be logged to the console.
Step 2.3 Invoke Deployed Function
We can invoke a deployed function in the same way we invoked our local function, just without the local
command:
sls invoke -f pulls -p data.json
(Optional) Step 2.4: Cleanup
If you have been following this tutorial and would like to clean up the resources you deployed, you can simply run:
sls remove
BE CAREFUL when running this command. This will delete your entire resource group.
Additional Steps
Stay tuned for future posts walking you through other steps of setting up your service, including adding API Management configuration, quality gates like linting and unit tests, adding Webpack support, CI/CD and more.
Also, if you're going to be at ServerlessConf 2019 in NYC, the Microsoft team is putting on a Azure Serverless Hands-on Workshop on October 7th from 8:30 am to 5:00 pm.
Contributing
We’re eager to get your feedback on the serverless-azure-functions
plugin. Please log issues on the GitHub repo with any bug reports or feature requests. Or better yet, fork the repo and open up a pull request!
Part Two of this tutorial can now be found here.