Introduction

This book is created with mdBook.

Trainings

As of now, there's just one for Azure Functions. If time allows, however, I'll add more.

How to work with the book

Select headlines as anchors

You can select headlines as anchors just by clicking on them. This will give a deep link to specific headline and I recommend it if you take a break or want to reference a specific part of the training.

Code blocks

Code blocks, commands and output snippet are embedded like:

{
  "this": "is a json example"
}

You can copy the code block into your local clipboard to allow pasting into another application.

Tasks

The book contains certain tasks where you're expected to do something. Some of them are marked as optional.

Tasks are marked like this: 🛠 TASK.

Task come with hints and solutions, but the former is optional. Just click on the text to reveal what's hidden.

💡 HINT

This is a hint!


🎓 SOLUTION

This is the solution!

Quizzes

Chapters might also contain quizzes.

A quiz is marked like this: Quiz

Quizzes are simple Q&As and work exactly like tasks.

Local use

You can also clone the repository and use mdBook locally.

Contribution

If you encounter any errors or have any questions, open up a ticket on GitHub.

Also, if you'd like to add trainings yourself, feel free to reach out to me.

Azure Functions

What you can and can not expect

This training is designed to give you a fundamental understanding of Azure Functions, how they work and what you can do with it as well as some best practices for developers.

This training is not designed as a exhaustive coding guide. There are samples, but they will be limited to showcase certain scenarios.

Note: The Azure Resources we're going to allocate are not free of charge. We'll clean them up after the training, however.

Prerequisites

Local Function App

Introduction

In this chapter we will create a local function app and take a closer look at its files.

Before we create our first Function App, let me point out an important distinction between the terms Azure Functions, Functions, Function App and function:

Azure Functions, Functions and Function App are used interchangeably in the documentation and refer to Azure's serverless compute service or more specific its runtime environment, whereas a function or functions (lower case) refer to a block of code executed in the runtime environment.

Install the Azure Function Core Tools

Azure Functions Core Tools let you develop and test your functions on your local computer from the command prompt or terminal. Your local functions can connect to live Azure services, and you can debug your functions on your local computer. You can even deploy a Function App to your Azure subscription.

Install the Azure Function Core Tools via the node package manager (npm):

npm install -g azure-functions-core-tools@4 --unsafe-perm true

Check if you can execute the func command:

func --version

Note: If you can't execute the func command, check the output from the installation command and your PATH environment variable.

Create a Function App

To create a new Function App in a directory called function-app-demo, type:

func init function-app-demo --worker-runtime node --language typescript

Change to the newly created directory and browse the devDependencies section in the package.json file. If the package @types/node is not installed in version 18 update it with:

npm install -D @types/node@18

Note: We're using the types for @types/node@18, because the version should match with the Node.js version mentioned in prerequisites.

The Function App's files

Let's take a look at the Function App-specific files in our new app.

The host.json

The host.json metadata file contains configuration options that affect all functions in a function app instance.

Browsing the file contents, you will notice two different version statements. The first one (root level) defines the Function Runtime version and the second one defines the Extension Bundle version.

Function Runtime, as the name implies, specifies the runtime version for the function. The runtime version defined in the host.json can be misleading, though, because it's specified as either version 1 or version 2 where the latter effectively means version 2 and above. The actual runtime version is defined by the Azure Functions Core Tools we're using.

Extension bundles are a way to add a pre-defined set of compatible binding extensions to your function app.

🛠 TASK: Update the extension bundle version

Update the extensionBundle version to version 4.

💡 HINT

Check out the documentation.


🎓 SOLUTION

The extensionBundle definition in the host.json should now look like:

  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.0.0, 5.0.0)"
  }

The local.settings.json

The local.settings.json represents the App settings for your local environment, which contain configuration options that affect all functions for that function app. These settings are accessed as environment variables.

As of now the file doesn't contain too many settings. However, there is a setting for our language worker runtime (FUNCTIONS_WORKER_RUNTIME) which corresponds to the language runtime being used in your application (in our case Node).

The .funcignore

A function app may contain language-specific files and directories that shouldn't be published. Excluded items are listed in the .funcignore file in the root directory.

Note: The file already includes the directories of the dev dependencies in our package.json file and a test directory. If you add further dependencies, you should make sure they are listed in the .funcignore file. This is something to keep in mind as it might slow down the publishing of new versions considerably if you upload test frameworks or other unwanted (and possibly large) dependencies to Azure.

Local Function

Introduction

In this chapter we will create a local function, look at its details and then start and call it.

Make sure you have created a Local Function App.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Add a function to our Function App

A function is the primary concept in Azure Functions.

A function contains two important pieces:

  • your code, which can be written in a variety of languages
  • its config file called function.json

For scripting languages, you must provide the config file yourself. For compiled languages, the config file is generated automatically from annotations in your code.

We can add a new one to our Function App with the Azure Functions Core Tools – take a look at the command reference for insights on the parameters:

func new --name greetings --authlevel anonymous --template "HTTP Trigger"

That's it! We've successfully added a new function 🎉!

When listing the files in our Function App root directory, you'll see a new directory, named like our function ("greetings"), which contains all the files for the function itself. So if you want to delete a function, it's enough to remove the directory.

Note: We won't go into the details of different authorization levels yet. For now use anonymous when you add new functions.

The function.json

The function.json file defines the function's trigger, bindings, and other configuration settings. The runtime uses this config file to determine the events to monitor and how to pass data into and return data from a function execution.

Triggers

Triggers cause a function to run. They define how a function is invoked and a function must have exactly one trigger. Triggers have associated data, which is often provided as the payload of the function.

In our case we created an HTTP Trigger, so our function is triggered via HTTP requests.

Bindings

Binding to a function are a way of declaratively connecting another resource to the function. Bindings may be connected as input bindings, output bindings, or both. Data from bindings is provided to the function as parameters.

Bindings are optional and a function might have one or multiple input and/or output bindings.

Details

Browsing the contents of our function.json file reveals we currently have two bindings.

Sample function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/greetings/index.js"
}
  • An in-binding (input) named req of type httpTrigger
  • An out-binding (output) named res of type http

The index.ts

The index.ts was created for us from the HTTP Trigger template we specified when adding the new function. It contains a sample function implementation with which we'll play around in a moment.

But first take a closer look at the constant called httpTrigger:

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {...}

There are some noteworthy things here:

First, the function is asynchronous. Review the language-specific details if that has any implications for you (i. e. TypeScript or Python)

Second, the function expects two parameters:

  • The first one (context) is language-specific, but other languages have equivalents for it. It's passed to every function and is used for receiving and sending binding data, logging, and communicating with the runtime.
    • The structure of the context object depends on the selected trigger and bindings.
  • The second one (req) is the http request object.
    • The name of the request object must match the name defined for the input binding in your function.json

Start the function

Now we can finally start our new function with:

npx tsc && func start

The command triggers the TypeScript compiler and starts the function afterwards.

The output contains details on the Core Tools version and the runtime version, but more importantly we get an overview over the functions we provide, their URL and the accepted http methods.

Sample output
Azure Functions Core Tools
Core Tools Version:       4.0.4915 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.14.0.19631


Functions:

        greetings: [GET,POST] http://localhost:7071/api/greetings

For detailed output, run func with --verbose flag.

Shutdown the function

To shutdown your Function App, press ctrl+c.

Verbose flag

The verbose flag can be helpful if you want to get insights what the runtime is doing under the hood.

Give it a try and start the function with the verbose flag:

npx tsc && func start --verbose

Note: The verbose flag toggles only the runtime log level, but not the log level of your functions. We'll learn how to toggle the function log levels later, though.

Calling the function

After starting the function app you can easily test it with any Rest client or open it in your browser:

curl http://localhost:7071/api/greetings

Or with parameters (browser):

curl http://localhost:7071/api/greetings -d '{"name":"codecentric"}'

Quiz

What file specifies the accepted http methods for a function of type HTTP Trigger?

Every function has it's dedicated settings file called function.json.


What error code does the Function send if you called it with an unspecified http method?

It responds with 404 Not Found.

Test command:

curl -X OPTION http://localhost:7071/api/greetings -v

Can you see error messages in the Function console output? Does the output change if you use the verbose flag to start the Function?

The console output does indeed not show unsuccessful attempts to call the function. That changes, however, if we restart the Function with the verbose flag.

Test command:

curl -X OPTION http://localhost:7071/api/greetings -v

Are there function bindings for Kafka? If so, which runtime version is supported? Are in, out or both binding types supported?

Take a look at the documentation.

As you can see Kafka is supported since runtime version 2.x. Furthermore, only output bindings are supported.

Azure Function App

Introduction

After successfully creating a local Function App and getting our first function up and running, it is now time to create a Function App in Azure.

The Azure resources will be created using a Bicep template (you don't need to install any additional software).

Login to your Azure account

First we need to log in to our Azure account with the Azure CLI.

Login the Azure CLI:

az login
Sample output
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize.
Please continue the login in the web browser.
If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
Opening in existing browser session.
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "some-tenant-uuid",
    "id": "your-subscription-uuid",
    "isDefault": true,
    "managedByTenants": [],
    "name": "you@sample.com",
    "state": "Enabled",
    "tenantId": "some-tenant-uuid",
    "user": {
      "name": "you@sample.com",
      "type": "user"
    }
  }
]

Note: If you have more than one subscription, use az account (list / set) to switch to the one you want to use.

Create a new Resource Group

A resource group is a container that holds related resources for an Azure solution.

You might want to change the location for the created resources to something close to you.

Check what locations are eligible with:

az account list-locations --query "[].name"

Create one with:

az group create --name rg-functions-demo --location westeurope

Note: It might take a while for the resources to show up in Azure Portal.

Creating the demo resources

The resources will be created in the same location you used for the resource group. However, not all resources are available for every resource. You can look up which products are available in which location here.

Download the template file from here.

Create the resources with:

az deployment group create --resource-group rg-functions-demo --template-file functions-demo.bicep

The following resources will be created:

  • A Function App is a serverless solution that allows you to implement your system's logic into readily available blocks of code.
  • A Storage Account contains all of your Azure Storage data objects, including blobs, file shares, queues, tables, and disks. In our case it's providing storage for our Function App.
  • An Application Insights instance is an extension of Azure Monitor and provides Application Performance Monitoring (also known as “APM”) features.
  • An App Service plan defines a set of compute resources for a web app to run. In our case it provides the execution environment for our functions.

Note: If you use an alternate language you need to update the Bicep template accordingly.

Deploying your App to Azure

Introduction

Now that we've created our Azure Resources, it's time to deploy our local Function App to its Azure counterpart.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Deployment best practices

When deploying a Function App, it's important to keep in mind that the unit of deployment for functions in Azure is the Function App. Meaning, all functions are deployed at the same time, usually from the same deployment package.

Check out the best practices documentation for further insights.

Also, think about if you have added new items to your Function App and if you need to update your .funcignore before deploying.

Deploying the Function App

Our Function App can be deployed with a single command. However, before we can execute it, we need to know the name of our Function App created by the Bicep template.

Look up the name:

az resource list --resource-group rg-functions-demo --query "[?kind=='functionapp,linux'].name"

Afterwards deploy your Function App with:

func azure functionapp publish <APP_NAME>
Sample output
Getting site publishing info...
Creating archive for current directory...
Uploading 427.34 KB [#############################################################################]
Upload completed successfully.
Deployment completed successfully.

Note: This might take a minute or two depending on your compute resources and internet connection.

Testing your deployment

After our deployment completed successfully, we'll now test if everything works as expected.

Query the enabled host names for your Function App with:

az functionapp show --resource-group rg-functions-demo --name <APP_NAME> --query "defaultHostName"

As Azure Websites get a default custom Domain name, where the application name is the third level of the domain name followed by .azurewebsites.net. That is also true for Azure Functions as they are treated as Websites. Furthermore the domain has a valid wildcard certificate, so we can call our resources via https.

From our local tests we also know that our functions are located in the api subdirectory followed by the function name.

So, your URL should look like:

https://<APP_NAME>.azurewebsites.net/api/greetings

Note: We don't need to specify a port, because the site listens on the default https port.

🛠 TASK (optional): Call the greetings API

Use the curl commands introduced in the chapter Local Functions or any other REST client to test the API.

💡 HINT
  • Look up the function name, as done above, and replace <function-app-name> in the url with it
  • Make sure to use https instead of http for non-local calls

Working with App Settings

Introduction

In the previous step we deployed our Function App to Azure. We'll now take a look at the App settings.

Application settings in a function app contain configuration options that affect all functions for that function app. By default, you store connection strings and secrets used by your function app and bindings as application settings and access them as environment variables. However, secrets not needed by Azure to be stored as application settings should rather be stored in Azure KeyVault instead of the application settings.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Fetch the settings from Azure

We learned in chapter 2.1 Local Function App that our local Function App settings are stored in a file called local.settings.json. When we created our local Function App, the file was added with the minimal set of entries the runtime needs to work properly.

Take a look at your local settings first:

func settings list

And compare it to the settings in Azure:

az functionapp config appsettings list --resource-group rg-functions-demo --name <APP_NAME>

As you can see, there are already more settings in Azure, even if we just created the app and deployed it once. For example, the settings for Application Insights were automatically set when we created the resources via Bicep.

To fetch the settings from Azure we can execute:

func azure functionapp fetch-app-settings <APP_NAME>

After executing the command, the local.settings.json is updated and matches the settings in Azure. If the file is, for whatever reason, not found on your machine, you can still execute the command and the file will be created for you.

Encrypting and decrypting

App settings and connection strings are stored encrypted in Azure. They're decrypted only before being injected into your app's process memory when the app starts.

The function runtime offers the possibility to encrypt and decrypt your local settings as well, and as these settings contain sensitive information (connection strings, secrets and so on) the simple advice is to encrypt them and only decrypt them if needed.

However, secrets not needed by Azure to be stored as application settings should rather be stored in Azure KeyVault instead of the application settings.

Encrypt the settings with:

func settings encrypt

Decrypt the settings with:

func settings decrypt

Use app settings in functions

Application settings can be accessed in functions like every other environment variable and are accessible in every function in the same Function App. Let's add a new function that returns all environment variables when it's called.

Add the function (use authlevel function):

func new --name settings --authlevel function --template "HTTP Trigger"

Update the code to return all environment variables (snippet):

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  context.res = {
    // this will return all environment variables
    body: process.env,
  };
};

Now start the Function App and call the settings endpoint. Interestingly you'll see not just the settings from the local.settings.json but also quite a lot that the runtime added by itself. Don't use environment variables not configured in local.settings.json in your function, however, because they aren't guaranteed.

Note: As application settings contain sensitive information you usually wouldn't return them like we did here and just use what's necessary where it's necessary in your functions.

🛠 TASK (optional): Deploy the Function App

Deploy the function app like you did in the previous chapter and call the new settings endpoint. You'll see that you won't get any output, but instead a 401 Unauthorized response. That's because we configured the authorization level to be function.

We'll learn how to call that function in a later chapter. For now we can just leave it like that as we made sure nobody besides us can call it.

Note: If you missed to create the function with authorization level set to function, just update the authLevel to function in the function.json and redeploy the Function App.

Quiz

Can the functions still be started if you delete the local.settings.json?

The Function App can still be executed, even though you'll get a warning:

Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]

Using the specific language parameter the warning disappears:

npx tsc && func start --typescript

But only the basic environment variables will be available like that. Refetch them from Azure before you continue.


What alternate secrets store should you consider for sensitive information instead of application settings?

Azure KeyVault is a secure alternative if your secrets are not required to be stored as application settings by Azure.

Adding health checks

Introduction

In this chapter we're going to add basic health check functionality to our Function App, which you can use to implement Azure Service Health alerts (not part of this training).

The hosting infrastructure for Azure Functions is provided by Azure App Service. Therefore the documentation for health checks is found in the Azure App Service documentation rather then for Azure Functions.

The Health Check feature can be used to monitor Function Apps on the Premium (Elastic Premium) and Dedicated (App Service) plans only. It's not an option for the Consumption plan, however, as the runtime for these Function Apps is only available when functions are called.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Creating a health check endpoint

Reading the documentation, we learn that creating an health check endpoint is rather simple.

  • our Function App needs an endpoint that returns HTTP status code 200 if everything is fine and 500 otherwise
  • the default path for that endpoint is called health

We also learn that health checks are usually used as an interface to probe other services our Function App depends on, like databases and so on. As we have no database available we need to substitute it.

But let's take one step at a time.

🛠 TASK: Create the endpoint

As you have done before, create a new function called health with authorization level anonymous.

💡 HINT

We already create a function like that before.


🎓 SOLUTION
func new --name health --authlevel anonymous --template "HTTP Trigger"

🛠 TASK: Update the implementation

As of now we don't have a bad path option, so let's just implement the happy path first. We want to return HTTP status code 200.

🎓 SOLUTION

Update the code (snippet):

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  context.res = {
    status: 200,
  };
};

🛠 TASK: Deploy the Function App

Let's deploy the Function App before we continue.

🎓 SOLUTION

Look up the name:

az resource list --resource-group rg-functions-demo --query "[?kind=='functionapp,linux'].name"

Afterwards deploy your Function App with:

func azure functionapp publish <APP_NAME>

🛠 TASK: Update the health check settings

Azure Functions won't automatically probe the health endpoint just because there's an implementation. So we need to update our Function App in order to make use of it.

Update the health check settings with:

az webapp config set --resource-group rg-functions-demo --name <APP_NAME> --generic-configurations '{"healthCheckPath": "/api/health/"}'

Note: In a real world scenario we wouldn't execute the command like that. Instead we would update our IaC template (bicep, terraform, you name it). However, you might have changed your Function App because you tested something or you're using an alternate language and updated the template and we might end up breaking your Function App if we would use an updated template – that's why we don't.

🛠 TASK (optional): Monitor the health check status

Before you can start monitoring the health check status of your Function App, you should take a little break, because it'll take a while before any metric is available.

So wait for at least 5 Minutes before you execute:

az monitor metrics list --resource-group rg-functions-demo --resource-type "Microsoft.Web/sites" --metric "HealthCheckStatus" --interval 5m --output table --resource <APP_NAME>
Sample output

The Average column might be empty if no metric was recorded before.

Timestamp            Name                 Average
-------------------  -------------------  ---------
2023-01-31 11:17:00  Health check status
2023-01-31 11:22:00  Health check status
2023-01-31 11:27:00  Health check status
2023-01-31 11:32:00  Health check status
2023-01-31 11:37:00  Health check status
2023-01-31 11:42:00  Health check status
2023-01-31 11:47:00  Health check status
2023-01-31 11:52:00  Health check status
2023-01-31 11:57:00  Health check status
2023-01-31 12:02:00  Health check status  33.333333333333336
2023-01-31 12:07:00  Health check status  100.0
2023-01-31 12:12:00  Health check status  100.0

Note: The output from the monitor command depends on the interval and the execution time. So depending on what interval you choose and what time you execute it you might get different results. If you want results based on a specific start time, you can set the --start-time parameter. Take a look at the az monitor metrics documentation for further options.

🛠 TASK: Prepare the unhappy path

As already mentioned, we don't have any database or other alternate service available which we could use to test our health check against.

So we're going to implement a function that allows us to toggle an environment variable to either true or false. Afterwards we can use that environment variable in our health check to toggle its state.

Create a function called toggles and implement it with the following traits:

  • the API expects either the query or the body to contain a variable called toggle
  • create a switch for that toggle
  • the switch either knows the toggle and toggles its value or it returns an HTTP status code 400
  • the toggle for the health state is called isHealthy and will toggle an environment variable called TOGGLE_IS_HEALTHY
  • valid values for the environment variable are "true" or "false"
  • if the environment variable is not undefined, default to "false"
  • respond with HTTP status code 200 and a message which value was toggled
🎓 SOLUTION

Create the function with:

func new --name toggles --authlevel anonymous --template "HTTP Trigger"

Update the code (snippet):

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const toggle = req.query.toggle || (req.body && req.body.toggle);
  var responseStatus = 200;
  var responseMessage = "";

  context.log(`Toggling: ${toggle}`);

  switch (toggle) {
    case "isHealthy":
      if (process.env.TOGGLE_IS_HEALTHY == "false") {
        process.env.TOGGLE_IS_HEALTHY = "true";
      } else {
        process.env.TOGGLE_IS_HEALTHY = "false";
      }
      responseMessage = `Toggled ${toggle} to ${process.env.TOGGLE_IS_HEALTHY}`;
      break;
    default:
      context.log.error(`Unknown toggle: ${toggle}`);
      responseStatus = 400;
      break;
  }

  context.res = {
    status: responseStatus,
    body: responseMessage,
  };
};

🛠 TASK: Update the health check function

Now that we can toggle our environment variable using a simple API called, it's time to use that variable in our health check function.

Update the function to respond with HTTP status code 500 if process.env.TOGGLE_IS_HEALTHY is "false", otherwise respond with HTTP status code 200.

🎓 SOLUTION

Update the code (snippet):

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const responseStatus = process.env.TOGGLE_IS_HEALTHY == "false" ? 500 : 200;
  context.res = {
    status: responseStatus,
  };
};

🛠 TASK (optional): Deploy, Toggle and Monitor the health status

You can now deploy your Function App as well as toggle and monitor the health status. Remember, though, it might take a while till the status is available.

You can also use Azure Service Health alerts to monitor your Function App in Azure Portal. As this is not part of our training, remember to clean up afterwards.

Note: Toggle the health state back to "true" when you're done or Azure will try to restart your Function after a while.

Quiz

What's the application setting WEBSITE_HEALTHCHECK_MAXPINGFAILURES used for and what's its default value (hint: App Service documentation)?

The default value is 10 and it's used to determine how many failed requests to the health check endpoint are valid, before the service is deemed unhealthy.

Working with Logs

Introduction

When it comes to logging, sadly an often undervalued topic, Azure Function Apps have quite a broad scope of different mechanisms available, including Streaming Logs, Diagnostic Logs, Scale controller logs and Azure Monitor metrics.

In the last chapter we were already using Azure Monitor metrics to query information about the health check status.

In this chapter, we're going to focus solely on the basics of Streaming Logs, which come in two flavours:

  • Built-in log streaming lets you view a stream of your application log files. This stream is equivalent to the output seen when you debug your functions during local development. This streaming method supports only a single instance and can't be used with an app running on Linux in a Consumption plan.
  • Live Metrics streaming lets you view log data and other metrics in near real time when your Function App is connected to Application Insights.

We're also going to look into how to run only specific functions, disabling functions and so on.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Look under the hood

Before we take a look at the logs, we take a little detour and talk a little about trace levels and log levels, learn the difference and write a little function to write some logs.

Trace levels

Trace levels define what kind of message we send to our logs stream.

One of the reasons why we're focusing solely on the basics of streaming logs is because trace levels in Azure Functions depend on your language of choice.

Nevertheless, the following four basic levels are available in most languages:

  1. Information: should be purely informative and not looking into them on a regular basis shouldn’t result in missing any important information. These logs should have long-term value.
  2. Warning: should be used when something unexpected happens, but the function can continue the work
  3. Error: should be used when the function hits an issue preventing one or more functionalities from properly executing
  4. Trace / Verbose: should be used for events considered to be useful during software debugging when more granular information is needed

In Typescript / Javascript we can access the logger via the context as shown in the table below.

Trace LevelWrite with
Informationcontext.log.info(message) or context.log(message)
Warningcontext.log.warn(message)
Errorcontext.log.error(message)
Verbosecontext.log.verbose(message)

We've already used context.log and if you took a closer look to the logs from the runtime process you should have already seen some of these messages.

Note: Don't mistake console.log for context.log. Because output from console.log is captured at the function app level, it's not tied to a specific function invocation and isn't displayed in a specific function's logs.

Log Levels

Log levels define what kind of messages we want to capture in our logs.

While there are only four trace levels, there are seven log levels. That's because the trace levels are language-specific, whereas the log levels are runtime-specific. The Function App runtime is written in .NET, which knows all seven levels for logs and traces.

The six log levels are Trace, Debug, Information, Warning, Error, Critical, None, whereas Trace is the highest level and None, of course, the lowest. The level you choose includes all lower levels, except for None which will deactivate logging completely. So, if you choose Warning, for example, you would also include Error and Critical log messages.

Log levels can be configured for different categories. The important ones for us right now are only Function and default, though. Function specifies the log level for your functions and default for all categories not configured otherwise.

The log levels can be configured in the host.json, so on the scale of the whole Function App. Nevertheless, we can specify the log level per function and will do so in a later task.

Sample host.json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    },
    "logLevel": {
      "default": "Information",
      "Host.Results": "Warning",
      "Function": "Trace",
      "Host.Aggregator": "Warning"
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.0.0, 5.0.0)"
  }
}

As already mentioned, log levels are runtime-specific, so changing the log level (for functions) will impact all your functions. We'll learn how to work around that later in this chapter.

Let's add the log levels to the table

Trace LevelWrite withLog Level
Informationcontext.log.info(message) or context.log(message)Information
Warningcontext.log.warn(message)Warning
Errorcontext.log.error(message)Error
Verbosecontext.log.verbose(message)Trace

The only surprise here is that Verbose correlates to Trace, not to Debug as one might have expected.

Note: The host.json is included when deploying the Function App, so remember to reset your settings before you deploy it. Or, as we do later in this chapter, overwrite the host.json settings in your local.settings.json as described here.

Creating a log function

Now, we want to add a function which automatically writes log data once in a while.

🛠 TASK: Create a Timer Trigger function

Let's add another function to write some log messages. But this time we'll create a Timer Trigger function rather then an HTTP Trigger.

A Timer Trigger is triggered at scheduled times. The schedule is defined via a Cron Expressions and the expression is configured in the function.json.

If you need a verbal interpretation of a cron expression, you can use a cron expression generator website.

Sample function.json
{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 */5 * * * *"
    }
  ],
  "scriptFile": "../dist/logs/index.js"
}

🎓 SOLUTION

The function can be easily created using the Timer Trigger template. The difference this time is, that we don't need to specify the authlevel, because the function is not called by an external source.

Create the function with:

func new --name logs --template "Timer trigger"

🛠 TASK: Update the function code

Next, we want to update the function code to write a log message for each trace level.

🎓 SOLUTION

Update the code (snippet):

const timerTrigger: AzureFunction = async function (
  context: Context,
  myTimer: any
): Promise<void> {
  const functionName = context.executionContext.functionName;

  context.log.verbose(`Timer trigger: ${JSON.stringify(myTimer)}`);

  context.log.info(`=> Information from function "${functionName}"`);
  context.log.warn(`=> Warning from function "${functionName}"`);
  context.log.error(`=> Error from function "${functionName}"`);
};

🛠 TASK: Configure which functions to run

The task is to start the Function App, but let it run the logs function only.

There're two possibilities to do so, you could either disable all other functions in the local.settings.json, or you could define which functions to run in the host.json. So it's either a denylist or an allowlist.

We want to use a allowlist, but we want to run just some local tests. So we don't want to change the host.json file because it's deployed to Azure. Therefore, we need a possibility to overwrite host.json configuration locally.

Check out the documentation and create an allowlist, including only the logs function.

Start the function, after changing the settings, to see the log messages. Depending on how patient, or rather how impatient you are, you might also want to update the cron schedule to run the function more often, or take a little break.

💡 HINT
  • You need overwrite host settings for functions
  • Overwritten host settings always start with AzureFunctionsJobHost
  • Next you need to follow the json path to the specific setting and replace every opening curly bracket ({) with tow underscores (__)
  • The settings for function is an array and arrays are are numbered starting with zero (0)

🎓 SOLUTION

Update the local.settings.json with:

func settings add "AzureFunctionsJobHost__functions__0" "logs"

For the sake of completion, disabling functions can be done with:

# Noteworthy is the difference on the first level.
# Function settings are overwritten starting with "AzureWebJobs"."FunctionName"...
func settings add "AzureWebJobs.greetings.Disabled" "true"

Note: Some types of logging buffer write to the log file, which can result in out of order events in the stream.

🛠 TASK: Update the log level

As you've seen in the previous task, the logs don't contain the verbose messages yet.

Update the log level to trace for our logs function, but not for any other function.

💡 HINT
  • You need overwrite host settings for logging
  • See the answer of the previous task for further details how to do so

🎓 SOLUTION

Update the host.json (snippet):

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      ...
    },
    "logLevel": {
      "Function.logs": "Trace",
    }
  },
  "extensionBundle": {
    ...
  }
}

For the sake of completion, updating the local.settings.json could be done with:

func settings add "AzureFunctionsJobHost__logging__LogLevel__Function.logs" "Trace"

Streaming logs

At long last, our logs function automatically writes some logs we can work with. Let's get into it!

Built-in log streaming

Built-in log streaming lets you view a stream of your application log files. This is not yet supported for Linux apps in the Consumption plan.

However, that's not an issue for us, because we're luckily using an App Service plan to run our functions.

Use the logstream command to show your Function App logs on the command line:

func azure functionapp logstream <APP_NAME>

Wait for a moment, until you get the message:

Starting Live Log Stream ---

Finally, publish your Function App to Azure.

You'll see some messages like:

[INFO]  Starting OpenBSD Secure Shell server: sshd.
[INFO]  Hosting environment: Production
[INFO]  Content root path: /azure-functions-host
[INFO]  Now listening on: http://[::]:80
[INFO]  Application started. Press Ctrl+C to shut down.
No new trace in the past 1 min(s).

These logs only show messages written by the Function App itself, but not for a specific function.

So let's stop the execution with Ctrl+c and move on.

Live Metrics streaming

Live Metrics streaming lets you view log data and other metrics in near real time. However, it only works for Function Apps connected to Application Insights. And, as you might have guessed, we already did that.

You can start the live metrics view with:

func azure functionapp logstream <APP_NAME> --browser

The command will open the the live metrics view for your function app in your browser and you can see your incoming requests as well as the messages from our logs function.

Note: The browser view might fail with Data is temporarily inaccessible. Try to deactivate your ad blocker, cookie blocker and so on for portal.azure.com and it should work as expected.

Query Application Insights

As final step in this chapter we want to query our log messages from Application Insights.

You can use either the query command, or the events show command to receive messages for your instance.

Let's look at both options in detail.

Note: New messages are not instantly displayed in Application Insights. You need to wait a couple of minutes for messages to be available.

🛠 TASK (optional): Query your instance name

You can skip this one if you didn't change the name of your Application Insights instance when you created our Azure resources.

If you changed the name, query it and replace ains-training-demo with your <INSTANCE_NAME> in the commands below.

🎓 SOLUTION

Query the name of your Application Insights instance:

az resource list --resource-group rg-functions-demo --query "[?type=='Microsoft.Insights/components'].name"

Using query

The command uses the Kusto Query Language (KQL), which you can use in Azure Portal for Azure Monitor as well as Azure Data Explorer.

Take the latest 2 results within the last 5 minutes with:

az monitor app-insights query --resource-group rg-functions-demo --app ains-training-demo --analytics-query 'traces | sort by timestamp desc | take 2' --offset 5m

The output is one or more tables represented as JSON. It's very powerful, but the output on the command line is not optimal for further processing.

When it comes to our trace levels, you can find the messages for a specific level by querying for the specific severityLevel.

We can query our verbose messages during the last 10 minutes with:

az monitor app-insights query --resource-group rg-functions-demo --app ains-training-demo --analytics-query 'traces | where severityLevel == 0 and operation_Name has "logs" | project message, timestamp | sort by timestamp desc' --offset 10m

You will see some of your Timer trigger messages written with context.log.verbose(message). If you updated the host.json to log verbose messages for the logs function, that is.

Using events show

The command can be used with the global --query parameter to filter the messages. It uses a JMESPath query string to filter the messages.

List messages from the last 5 minutes with:

az monitor app-insights events show --resource-group rg-functions-demo --app ains-training-demo --type traces --offset 5m

We can queries our verbose messages, during the last 10 minutes with:

az monitor app-insights events show --resource-group rg-functions-demo --app ains-training-demo --type traces --offset 10m --query "value[?operation.name=='logs'].[trace, timestamp]"

Note: A recommended alternative, especially if you work with more CLIs using JSON as output format, is using jq and bat. It's more fun with these two extraordinary tools.

Basic Function Security

Introduction

The aim of this chapter is to give you an understanding of the most fundamental security mechanisms of Azure Functions, or more specific, on how to use Function Access Keys.

Azure Function security in general is a much broader topic, which we can't possibly explain in its entirety in this training. That said, I strongly encourage you to read the whole documentation of Azure Functions security concepts after completing this chapter.

Commands in this chapter are to be executed in the Function App root directory unless stated otherwise.

Function Access Keys

As the name implies, Function Access Keys can be used to secure your functions in a manner where your requests must include an API access key in the request. Unless the HTTP access level on an HTTP triggered function is set to anonymous, that is.

They're good enough for trainings, demos, or development purposes but shouldn't be used in production.

Nevertheless they are the default security mechanisms Azure Functions provide and that's why we're learning about them. In fact, we already used Function Access keys back in Chapter 2.5, were we created a function with authorization level function, and they're also the reason why we created most of our functions with authorization level anonymous.

So, as you might have already guessed, authorization levels are coupled to access keys.

Possible authorization levels are:

  • anonymous: functions can be called without providing an API access key.
  • function: functions can be called providing an API access key of scope Function or Host.
  • admin: functions can be called providing an API access key of scope Admin.

Possible key scopes are:

  • Function: These keys apply only to the specific functions under which they're defined.
  • Host: These keys apply to all functions within the Function App (host-level).
  • Admin: Each Function App also has an admin-level host key. In addition to providing host-level access to all functions in the app, the master key also provides administrative access to the runtime REST APIs.
  • System: These keys are required by certain function extensions and the scope of system keys is determined by the extension, but it generally applies to the entire function app (host-level).

As a developer you'll most likely use authorization level function with either key scope Function or Host.

List Function App keys

Let's take a look at the keys our Function App provides:

az functionapp keys list --resource-group rg-functions-demo --name <APP_NAME>
Sample output
{
  "functionKeys": {
    "default": "<some-default-key>"
  },
  "masterKey": "<some-master-key>",
  "systemKeys": {}
}

As you can see, there's only one function key and the master key so far.

Calling functions using keys

Let's call our settings function with using anonymous and the different keys.

Anonymous:

curl 'https://<APP_NAME>.azurewebsites.net/api/settings' -v

Using a key:

curl 'https://<APP_NAME>.azurewebsites.net/api/settings?code=<KEY>'

Using a anonymous call, the function response is 401 Unauthorized, whereas it works as expected if we provide the key.

🛠 TASK (optional): Verify the admin key access

Update the function.json of your settings function, and set the authorization level to admin instead of function.

Redeploy the Function App and try using the function key and the admin key for the call.

What're the results?

🎓 SOLUTION

Update the authLevel in the function.json (snippet):

{
  "bindings": [
    {
      "authLevel": "Admin",
      "type": "httpTrigger",
      "direction": "in",
      ...
    }
    ...
  ]
}

Redeploy with:

func azure functionapp publish <APP_NAME>

Summary: The function key, doesn't work anymore, whereas the admin key works as expected.

Function-specific keys

So far, we've used a host key or the admin key, where both can access all APIs of our Function App. But, as we've learned above, we can also create keys for specific functions.

Let's create one for our greetings function:

az functionapp function keys set --resource-group rg-functions-demo  --function-name greetings --key-name GreetingsKey --name <APP_NAME>
Sample output

Output (snippet):

{
  ...
  "name": "GreetingsKey",
  "resourceGroup": "rg-functions-demo",
  "type": "Microsoft.Web/sites/functions/keys",
  "value": "<your-new-key>"
}

If we list our keys again, we won't find the key we just created, because it's function-specific.

We need to use the function specific command to list the keys for a function.

az functionapp function keys list --resource-group rg-functions-demo --function-name greetings --name <APP_NAME>

Note: We use az functionapp keys list ... for the Function App, but az functionapp function keys list ... for a specific function. The keys using the first command have the Host scope, the keys using the second command have the Function scope.

🛠 TASK (optional): Verify the function scope access

This task contains of three easy questions.

  1. Using the function key, we just created, can you access the greetings function with it?
  2. Using the function key, we just created, can you access the settings function with it?
  3. Can you still access the greetings function with the function key if you set its authorization level to admin?
🎓 SOLUTION

Question 1: Calling the greetings function with the key it works as expected and results in a 200 OK. The function should be anonymous right now, so it doesn't care about the key at all.

Question 2: As one would expect it results in 401 Unauthorized. Again nothing unexpected because the key is not valid for the settings function.

Question 3: This results in 401 Unauthorized because a function with authorization level admin can only be called using the master key.

Cleanup

This is the last chapter for this course and without further ado, we'll just clean up the resources.

Make sure you're in the right subscription:

az account show

Change the subscription if needed with:

az account list
az account set --subscription <SUBSCRIPTION_ID>

Drop it like it's hot (this might take a while):

az group delete --name rg-functions-demo --yes

That's it, well done!

I hope you enjoyed the training.