Everything you need to know to write, debug, deploy and monitor Azure Functions
1. Introduction
Learning how to correctly write and deploy with Azure Function beyond a “Hello Azure Function” is difficult. It is that way because although Azure documentation is plentiful but just not well organized. Conceptually it’s actually easy, if the documentation is not such a big mess. Recently I had to re-architect some code by taking the Azure Function approach. Originally these code were in the StartUp.cs
of an ASP.NET WebAPI
like so (simplified for illustration purpose):
public void Configure(...) {
//...
Task.Factory.StartNew(() => {
while (true) {
//Capture some data
//...
//Now waits for 15 minutes or so
Thread.Sleep(new TimeSpan(someInterval));
}
});
}
The code that captures some data is now to be moved to multiple Azure function apps. They will be triggered by timers. In this article, I want to share all lessons learned. I will show the complete process of how to go from zero to deployment and even beyond.
The goals are as follow:
- First, this approach will be as light weight as possible. NO massive Visual Studio 2017 installation is required.
- During development, since you’ll need to know how to debug and log messages, we’ll cover that. After the Azure function apps are deployed to the Azure Cloud, we certainly want to monitor log messages so that we can determine how they’re running or not running. We’ll cover that as well.
- In real-world situations like in my cases, we need to write and run multiple Azure function apps in production. These function apps need to share some common code such as Entity Framework data access, models, helper utilities, etc.
- We’ll cover how to do Dependency Injection.
Some assumptions:
- From this article, you will learn how to implement Azure function apps and run locally. But to get the maximum benefits when it comes to deployment on Azure, you should already have an Azure subscription. You should be familiar with portal.azure.com already.
- I will only show Azure function apps in C#. These functions are timer-triggered.
2. Set up NodeJS and npm packages
Everything above sounds perfect? Well, since we know nothing is perfect, so is this approach. The drawback, if you so consider, is that we need to install NodeJS (since we only do C# here). For me, it’s not a problem because I already have Node installed for many other purposes. Anyway, you can install from
https://nodejs.org/download/release/
Then we need to install the npm pckageazure-functions-core-tools
. This package will be required later. If we don’t do it now, Visual Studio Code will remind us later. For now, let’s just install and get it out of the way.
$ npm install -g azure-functions-core-tools@latest
3. Install .NET Core and Visual Studio Code
- Download the .NET Core SDK (2.1 at the time of this writing). Be sure to choose the SDK and NOT the Runtime, because we don’t want to just run the apps, we want to build the apps. You might already have this installed, in which case, this step can be ignored. Make sure when you type
dotnet
on a Terminal, the command is recognized.
- NO Visual Studio 2017 is needed. But we will need to install Visual Studio Code (hereinafter referred to as Code). Code is much more lighter weight than Visual Studio 2017 (which takes up more than 20 GB easily with only a few modules installed) and Code is also a a lot more stable. It does not hang every now and then like Visual Studio 2017. So Code is what we will use. If you don’t already have Code installed, the link is below. You might already have this installed, in which case, this step can be ignored.
- Install the
Azure Functions extension
in Visual Studio Code. In case you don’t know about Extensions, look for the 5th menu item in the most left gutter panel of Code. A by product of this installation isAzure Account extension
is also installed. - There are a couple ways to create an Azure Function: using the Command Line Interface (CLI) or directly from Code. I normally prefer CLI everything, but in this case, let’s do from Code.
- First, let’s create a new Project.
- Select a folder, choose C#. You should see a
.csproj
file created. Open this file and make sure the target framework is 2.1. If you don’t see 2.1, you might see problem with deploying later on in the process. I think because at the time of this writing, 2.1 is the default on Azure. So anything other than that doesn’t work. I experienced that, but the error message doesn’t help.
<TargetFramework>netcoreapp2.1</TargetFramework>
- Next let’s create a couple of function apps. Use the next icon.
- Select the same folder, choose
TimerTrigger
. For the function name, just call themFunctionApp1
andFunctionApp2
. Type any namespace you want. For this example, I nameExampleAzFuncs
as the namespace. - Specify any timer interval you want. I specify
0 */1 * * * *
(that’s 1 minute). To learn about the syntax, refer to https://en.wikipedia.org/wiki/Cron#CRON_expression - You might see multiple warnings from Code. If you see the one below, just click
Install
.
- If you see
- That means you have not installed NodeJS and the npm package
azure-functions-core-tools
as discussed in 2 above, now is the time to do. You can download node from https://nodejs.org/download/release/. After Node is installed, run the following command:
$ npm install -g azure-functions-core-tools@latest
- If you see
For any of triggers other than HttpTrigger
, AzureWebJobStorage
is required (We picked TimerTrigger
). Skip for Now. Just close the box. We’ll deal with this soon.
- If you get the warning
There are unresolved dependencies. Please execute the restore command to continue
just go ahead and clickRestore
. You might also get these warning at various times, especially after adding a nuget package. Just clickRestore
each time.
4. Restructure the folder hierarchy
At this point you should see in your Code Explorer panel a folder hierarchy similar to this
- You want to restructure by adding 2 folders at the top level. Name these folders
FunctionApp1
andFunctionApp2
. MoveFunctionApp1.cs
andFunctionApp2.cs
inside each of them respectively. In addition, in bothFunctionApp1
andFunctionApp2
folders, create a file in each namedfunction.json
. At the same level withFunctionApp1
andFunctionApp2
folders, create a folder namedSharedCode
. The folder hierarchy at this point should look like:
- Copy the following code to the
function.json
file. Change the name accordingly.
{
"bindings": [{
"name": "FunctionApp1or2",
"type": "timerTrigger",
"direction": "in",
"runOnStartup": true,
"schedule": "*/1 * * * * *"
}]
}
- Right now, there’s nothing in the
SharedCode
folder. But this is where we will place all the shared code.
5. AzureWebJobStorage
The reason we skip the above configuration of the AzureWebJobStorage
is we don’t want to point to an environment on the Azure cloud, not just yet. Since we’re still getting started and still in development, we can emulate the AzureWebJobStorage
locally. There are 2 options:
a. Standalone Storage Emulator
b. Azurite
Both are easy to use. Azurite is advantageous because it’s cross-platform. For me, I choose Azurite.
Regardless of whether you choose the Standalone Storage Emulator or Azurite, locate the file local.settings.json
and add the value UseDevelopmentStorage=true
for the key AzureWebJobsStorage
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}
Now open up the Terminal and you can run
$ func start --build
Depending on the interval you specify, every 1 or 5 second, you should see the some output like (of course you must have changed from the default log messages earlier) FunctionApp1 function executed at …
Some Troubleshooting tips
a. Missing value for AzureWebJobsStorage
If you see the error above, make sure the line "AzureWebJobsStorage": "UseDevelopmentStorage=true”
, is in the local.settings.json
file.
b. Emulator or Azurite not running
If you see the error above, make sure the Storage Emulator or Azurite is running
6. Logging
You saw the line of code log.LogInformation
above. ILogger
is injected to the Run
method, which makes it very convenient
public static void Run([TimerTrigger("0 */2 * * * *")]TimerInfo myTimer, ILogger log) {
log.LogInformation("...");
}
If you play with Code Intellisense, you’ll see other methods for logging such as log.LogCritical
, log.LogDebug
, log.LogError
, log.LogWarning
, etc.
To control the log level, locate the host.json
file and add the following lines in bold:
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Error"
}
}
}
7. Logging in other classes’ methods
Chances are nobody will write all the code in this single Run()
method, because that will be such a monolithic way of programming. So if we add more classes and want to log messages in their methods, how would we need to do?
We need to inject something like ILogger
(as seen in the Run
method).
8. Dependency Injection
We will revisit on how to log messages in other classes’ methods. For now we will need to do Dependency Injection in Azure Function. We will use Autofac, but because we’re doing Azure Function, we need to use Azure Function Autofac
. Open up the Terminal and run the command:
$ dotnet add package AzureFunctions.Autofac --version 3.0.5
Now add a new file inside the SharedCode
folder and call it something like AutofacConfig.cs
with the following content:
using System;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Extensions.DependencyInjection;
using Autofac;
using AzureFunctions.Autofac.Configuration;
using Microsoft.Extensions.Logging;
namespace ExampleAzFuncs {
public class AutofacConfig {
public AutofacConfig(string functionName) {
DependencyInjection.Initialize(builder => {
builder.Register(ctx => new ServiceCollection()
.AddLogging()
.BuildServiceProvider()
.GetService<ILoggerFactory>())
.As<ILoggerFactory>();
}, functionName);
}
}
}
Now let’s say we need to write some ServiceHelper
class. From this point on, it is assumed that any code that’s common to all the function apps will be placed in the SharedCode
folder. Let’s write the interface called IServiceHelper
and this implementation class ServiceHelper
. Create 2 new files called IServiceHelper.cs
and ServiceHelper.cs
with the following contents respectively:
//IServiceHelper.cs
namespace ExampleAzFuncs {
public interface IServiceHelper {
string Greet(string message);
}
}
and
//ServiceHelper.cs
namespace ExampleAzFuncs {
public class ServiceHelper : IServiceHelper {
public string Greet(string message) {
//We'll add logging later
return "Hello "+ message;
}
}
}
As you can see, there’s no logging code in ServiceHelper
yet.
Now we need to register with Autofac, so let’s revise the AutofacConfig constructor
above:
public AutofacConfig(string functionName) {
DependencyInjection.Initialize(builder => {
builder.RegisterType<ServiceHelper>().As<IServiceHelper>();
builder.Register(ctx => new ServiceCollection()
.AddLogging()
.BuildServiceProvider()
.GetService<ILoggerFactory>())
.As<ILoggerFactory>();
}, functionName);
}
Next, install the following nuget package from the Terminal
$ dotnet add package Microsoft.Extensions.Logging.Console --version 2.1.1
(Above should be all in one line)
Inject ILoggerFactory
to the constructor of ServiceHelper
and start logging.
//ServiceHelper.cs
using AzureFunctions.Autofac;
using AzureFunctions.Autofac.Configuration;
using Microsoft.Extensions.Logging;
namespace ExampleAzFuncs {
[DependencyInjectionConfig(typeof(AutofacConfig))]
public class ServiceHelper : IServiceHelper {
private ILogger _logger;
public ServiceHelper([Inject] ILoggerFactory logFactory){
_logger = logFactory.AddConsole(LogLevel.Information)
.CreateLogger<ServiceHelper>();
}
public string Greet(string message) {
_logger.LogInformation($"[{System.DateTime.Now}] Message is {message}");
return "Hello "+ message;
}
}
}
Update 11–5–2018:
The above logging code does not cause messages to be streamed from Azure once it’s running in production. Replace with the following:
Don’t forget to add the package
$ dotnet add package Microsoft.Extensions.Logging.AzureAppServices — version 2.1.1
End of update 11–5–2018
Inject IServiceHelper
to the Run method of the Azure Function:
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using AzureFunctions.Autofac;
using AzureFunctions.Autofac.Configuration;namespace ExampleAzFuncs {
[DependencyInjectionConfig(typeof(AutofacConfig))]
public static class FunctionApp1 {
[FunctionName("FunctionApp1")]
public static void Run(
[TimerTrigger("0 */1 * * * *")]TimerInfo myTimer,
ILogger log,
[Inject] IServiceHelper serviceHelper) {
serviceHelper.Greet("Azure Function");
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
}
}
}
The result would look something like
9. Debug locally
Logging messages is great for troubleshooting but sometime debug is more preferable. Debugging an Azure Function in Code could not be easier (but as a side note took me hours to figure out). Simply cursor to the line where you want to place the breakpoint, then hit F9. To run, hit F5.
However, if inside the Run
method, we need to make some asynchronous calls, that means we have to mark the calls with await
, then Run has to be marked with async
. Doing so, then we will no longer be able to set breakpoints and debug.
10. Deploy to Azure
My preference is to use the Continuous Deployment approach to deploy all of my Azure App Services and Azure Functions. But we’ll do directly for now.
- Click on the up arrow in blue.
Create a New Function App
, type in the name for the Azure Function. I typedExampleAzFuncs
.- Create a
New Resource Group
or select an existing one. - Create a
New Storage Account
or select an existing one. If you remember, during developement when we run locally, we tell the configuration inlocal.settings.json
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
Azurite
or the Storage Emulator
took care of providing the storage. Now when we deploy to Azure, we need real Azure WebJobs Storage. By using the extension, we can easily configure it on Azure.
Everything should go smoothly. If you get the following error Could not zip a non-exist file path
, chances are you didn’t specify 2.1 for the target framework as mentioned above.
11. Logging in Production
To view logging messages, right click on your Function app and select Start/Stop Streaming Logs.
12. Conclusion
The article might seem long, but that’s because I’m too verbose. But as you can see, it’s not that conceptually hard. If you like, please clap.