“Friends don’t let friends right-click publish”Damian Brady, in a blog post from February 2018
I’m sure you’ve heard it before: you shouldn’t be deploying production-grade applications from Visual Studio. And if you’re not convinced of that yet, stop reading this, head over to Damian’s post on the subject, and pop back over once you’re ready to get serious about how you build and deploy your Azure Functions projects.
The tooling for Azure Functions is improving with every passing day. But a step by step guide would likely be outdated the moment it’s published. Instead, I’ve put together a blueprint of what a CI/CD pipeline for Azure Functions should look like. I’ll mention some tools and products along the way, but the goal of this article is to explain the process, rather than the implementation details.
Taking a local Azure Functions project and running it in the cloud can be accomplished with a few simple steps. The ultimate goal is to enable rapid and reliable deployment of your functions, time after time.
Here’s what an ideal build and deployment process looks like:
Of course, not every step is needed on day one. In the next few sections, I’ll break down what’s happening every step of the way so that you can make an informed decision on how you want to deploy your functions.
The goal of the build process is to take an Azure Function project that you’ve developed in Visual Studio and transform it into a deployable Function app artifact. As I’ve explained previously, the Function app groups the individual functions in the project into a single unit of deployment on Azure.
Create a Function app Artifact
The .NET CLI does the brunt of the work when you call
dotnet publish on the project containing your functions. The command restores packages, compiles the functions, and outputs all the files that are needed to run the Function app into a single folder.
The build process is where unit tests should be run. Ideally, you’re unit testing the classes that contain the function triggers, along with any other classes that contain business logic.
Writing unit tests for a class that contains purely business logic is just like you’d do elsewhere in .NET. On the other hand, until recently, it wasn’t easy to test the function trigger classes. Up until recently, there was no easy way to mock out dependencies. But all that has changed now that dependency injection is supported for .NET Core functions. There are no more excuses, get on those tests!
There are no restrictions from a test runner perspective. Whether you prefer MsTest, nUnit or xUnit, you’re all set. As far as mocking frameworks go, the only requirement is that it supports .NET Core. I use Moq, but nSubstitute works just as well.
The build process must fail when any of the unit tests fail. The tests are there to maintain a certain level of quality, and a failing unit test indicates a problem that needs to be fixed before going any further.
Zip Output Folder
Lastly, the output folder of the
dotnet publish command needs to be zipped up into a single archive for deployment. With that done, we’re ready to move on to the deployment process.
Build Process Summary
When: A commit is made to a long-lived branch, typically via Pull Request.
Frequency: Once per commit to the long-lived branch.
Steps: Build. Unit Test. Archive output folder into Zip file.
Objective: Each build creates a deployable Function app artifact. The same artifact will be used to deploy across the development, staging, and production environments.
Deploying a project from development through to production involves transforming the build artifact, pushing it to Azure, and then verifying that the application is running as expected. Some of these steps are trivial, while others require some forwarding thinking. But in all cases, using the right tool for the job makes all the difference.
A Function app is configured through two JSON files,
local.settings.json. Together, they contain the bulk of a Function app’s settings.
host.json file is for tweaking the function runtime behaviour. The file is included in the output folder when you build an Azure Functions project with
dotnet publish, and the settings it contains will be respected by the Function app in Azure. The problem is that in some cases you’ll want to use different settings across your local, development, staging and production environments. To get around this, you’ll need to perform JSON substitutions on the file, replacing the values based on the environment in question.
local.settings.json file is mostly used to store custom key/value pairs that are needed by the functions. What’s different about this file is that its settings are only used when a Function app is running on your local machine. The file will be included in the output of a
dotnet publish, but the values it contains will be ignored by the functions runtime in Azure.
So how do you make these settings stick in the cloud? The answer is through Configuration Settings, which can be seen — and edited — in the Azure portal when you click through to the Function app’s Platform features page.
The Azure CLI can also be used to add Configuration Settings manually, but some tools, like Azure DevOps, have some handy helpers that do the heavy lifting for you. I’ll talk about this a bit more in the deployment step below.
With the configuration step out of the way, we’re one step closer to actually deploying. But wait! We still need somewhere to deploy our artifact on Azure, and that somewhere is a Function app resource.
Creating a Function app is a one-time thing. You can do this step manually through the Create Resource option in the portal, via the Azure CLI, or by way of an ARM template. They all work equally well.
Once you’ve got a running Function app to receive your compiled code, it’s time to push it to the cloud.
There are no shortage of ways to go about getting your compiled and configured artifact running in Azure. Both the CLI and API can be used to manually deploy Function app artifacts to Azure.
But when it comes to production-grade work, do yourself a favour and use a well-known Continuous Deployment tool, such as Azure Devops or Octopus Deploy. Both have first-class support for Functions, giving you access to everything you need to deploy and configure your Function app.
There’s a neat shortcut to creating your build and deployment pipelines with Azure DevOps. Go to the Platform features page, and you’ll notice the Deployment Center option. Here you can to tell Azure DevOps to create you a Build Pipeline and Release Pipeline definition, so long as your code lives on one of GitHub, Azure Repos, or BitBucket. Once the definitions are created, you can modify it to include any additional steps you need.
With the Function app deployed and running in the cloud, it’s time to run some final verifications against the live application.
Acceptance tests differ greatly from unit tests. Unit tests are focused on the behaviour of the individual methods, while acceptance tests validate the function as a whole.
Acceptance tests are much harder to write than unit tests, mostly because they affect any underlying state. For example, let’s say you have an HTTP Trigger that writes an order to a database. The acceptance test is going to invoke the live endpoint, and the order will get written to the database as if it were a real order.
You’ll need to put in some thought as to how to handle this from a code perspective. Of course, you can create fake test accounts to generate the data, but how do you distinguish a real order from a fake one? What’s going to happen when a fake order is placed? Do you go through the entire order process? It’s a complex topic, which I won’t get into now, but will address in a future article.
Steve Smith wrote a great article on the implementation details of these types of tests. Make sure to give it a read if you’re thinking of writing acceptance tests for your functions.
In an ideal world, you have both unit and acceptance tests to provide good coverage of your application. But let’s face it, things are rarely ideal. So if you’re going to choose one set of tests over the other, favour the acceptance tests (I’m sorry, Uncle Bob). The acceptance tests make frequent deployments possible — without them, you have to rely on manual checks to validate your deployments.
Rinse and Repeat
The deployment steps above repeat for every environment to which you want to deploy your Function app. If you’ve a development, staging and production environment, then these steps need to happen on each environment.
Deployment Process Summary
When: A Function App artifact has been built successfully.
Frequency: Once for each environment (development, staging, production).
Steps: Configure. Create Function App. Deploy. Acceptance Test. Rinse & Repeat.
Objective: Take a Function app artifact, deploy and test it in Azure.
A Word on Tooling
As of the time of writing, in late July 2019, the best way to build and deploy Azure Functions is with Azure DevOps. The custom tasks in both the Build and Release Pipelines make it a synch to build, deploy, configure and test your functions.
Don’t even think about building your own deployment tooling. The investment in time and money just isn’t worth it, especially when better tools already exist.
Creating a complete build and deployment pipeline for Azure Functions isn’t hard. But there is a fair amount of work that needs to happen so that your project is set up for success. The payoff is huge: you can deploy to production with the certainty that what you’ve worked so hard to build is functional from A to Z.