Azure Functions

This post is general notes and overview of Azure Functions.

 

Background

Azure Functions is the serverless compute service in Azure. It is a combination of Events and Code where events could be:

  • Time
  • Data
  • Web

In contrast to Azure Functions, we could use Azure App Service (Azure Web Applications and Web Jobs) but that still requires underlying hardware which has cost. That cost depends on the Hosting Plan selected for that App Service. App Service also requires configuration to do things scaling and redundancy. The Web Jobs part of App Service is actually the foundation for Azure Functions. The functions run inside a “Function App” inside Web App/Web Job.

 

Create a Function

When creating an Azure Function we can select a Hosting Plan. Here we can use Consumption Plan or App Service Plan. This is what determines the pricing model for the function. When selecting the Consumption Plan the cost will be per month at a pay per execution time. There is also a grant for 1 million requests per month. For use cases like web applications, this should be cheaper than using an App Service Plan. The App Service Plan is a cost per underlying hardware which is constantly running.

https://azure.microsoft.com/en-us/pricing/details/functions/

We can also select Storage Account for the function. It will use this account to create a file store for the actual files used by the function(s).

A basic Azure Function has two files – function.json and run.csx (using C#). The json file defines how the function will execute whereas the csx file is the actual function code. Below is template starter code sample:

#r "Newtonsoft.Json"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;

public static async Task Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    string name = req.Query["name"];

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;

    return name != null
        ? (ActionResult)new OkObjectResult($"Hello, {name}")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

The corresponding json file

{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

Note that the function has a unique URL which is under the Function Apps’ url. So for example, I’ve created a “Function App” called ‘johnstesting’. This app has a base url here:

https://johnstesting.azurewebsites.net/

Within this app I’ve created an HTTP based function and that created the template files that were shown above. This function has it’s own unique url here:

https://johnstesting.azurewebsites.net/api/HttpTrigger1?code=A8dfBra6/CTmt/iIFodjGfPsZBEt9Q0yTaG4QKCysThRDgxqavGYrA==&name=hello

The template function looks for a ‘name’ request parameter which I include in the url above. Pasting that in a browser does an HTTP GET which returns the result of “Hello, hello”.

 

Configuration

Since Azure Functions is built on top of App Service, many of the App Service configuration features are available. The Function App has two configurations – Function App Settings and Application Settings. The Function App Settings define things like

  • Enable / Disable the function
  • Function Runtime version
  • Host Keys
  • Restricting use / setting a quota

The Application Settings for Function Apps come directly from App Service. This includes things like

  • App Setting Variables (like connection strings)
  • Default Mappings
  • Debugging (remote)
  • It also shows Hot/Auto Swapping, which is an App Service feature to help with different deployment environments

 

Triggers and Bindings

Triggers are what cause a function to run. A trigger defines 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.

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

https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings

There are several types of triggers for starting Azure Functions. These are changing frequently. Some of these are triggers support binding. See the link above for complete list. Some popular trigger/bindings are:

  • HTTP and Webhooks
  • CosmosDB
  • Event Grid
  • Event Hubs
  • Table Storage
  • Timer

When using HTTP triggers we have the following settings:

  • Allowed HTTP Methods
  • Mode
    • Standard (input can be anything ex json, xml)
    • Webhook – requires a webhook type that will define the connection
  • Route Template (default is ‘api/functionName’)

 

Development and Deployment

There are 3 ways to develop/deploy Azure Functions

  • In Portal
  • CLI – an NPM package called Azure-Function-CLI
  • Visual Studio (2015+)

Deployment can be done in numerous ways. These include

  • Kudu, FTP, Web Deploy
  • From Visual Studio
  • Git Repository
  • Mercurial, Dropbox

When working with Azure Functions in Visual Studio we can do remote (or local) debugging.

 

Sample Project

The following is a sample project using Azure Functions, SQL Database, CosmosDB and MailMessage. The function is triggered by a timer. It tries to read some data from and then send out an email. If successfully, it logs the email into CosmosDB (Document Store).

Note that the connection strings and other variables are set as Environment Variables in the App Service’s Application Settings.

 

function.json

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 0 * * SAT"
    },
    {
      "type": "cosmosDB",
      "name": "outputDocument",
      "databaseName": "myDB",
      "collectionName": "myCollection",
      "createIfNotExists": false,
      "connectionStringSetting": "CosmosDbConnection",
      "partitionKey": "datatime",
      "direction": "out"
    }
  ]
}

 

run.csx

using System;
using System.Data.SqlClient;
using System.Globalization;
using System.Net.Mail;
using System.Threading.Tasks;

public static void Run(TimerInfo myTimer, out object outputDocument, ILogger log)
{
    log.LogInformation($"*** My Function ***");

    var connstr = Environment.GetEnvironmentVariable("SqlDbConnection");
    var today = DateTime.Now;
    var sender = Environment.GetEnvironmentVariable("sender");
    var recipients = Environment.GetEnvironmentVariable("recipients");
    var subject = $"My Function";
    var message = $"The sample function has successfully completed.";
    // Get data from SQL Database
    using (SqlConnection conn = new SqlConnection(connstr))
    {
        conn.Open();
        var query = $"SELECT column1 FROM sampleTable WHERE column1 > 0";

        SqlCommand command = new SqlCommand(query, conn);
        SqlDataReader reader = command.ExecuteReader();
        while (reader.Read())
        {
            message += reader[0];
        }

        conn.Close();
    }    

    // Send the email
    var result = SendEmail(sender, recipients, subject, message);

    // Save email to cosmosdb
    outputDocument = new
    {
        datetime = today,
        status = result,
        recipients = recipients,
        subject = subject,
        body = message
    };
}

public static string SendEmail(string sender, string recipients, string subject, string message)
{
    string server = Environment.GetEnvironmentVariable("smtp_server");
    int port = Convert.ToInt32(Environment.GetEnvironmentVariable("smtp_port"));
    string login = Environment.GetEnvironmentVariable("smtp_login");
    string password = Environment.GetEnvironmentVariable("smtp_password");
    
    MailMessage mail = new MailMessage();
    mail.From = new MailAddress(sender);
    foreach (var recipient in recipients.Split(new [] {";"}, StringSplitOptions.RemoveEmptyEntries))
    {
        mail.To.Add(recipient);    
    }
    mail.Subject = subject;    
    mail.Body = message;

    SmtpClient client = new SmtpClient();
    client.EnableSsl = true;
    client.Host = server;
    client.Port = port;
    client.DeliveryMethod = SmtpDeliveryMethod.Network;
    client.UseDefaultCredentials = false;
    client.Credentials = new System.Net.NetworkCredential(login, password);
    
    try {
        client.Send(mail);
        
        return "Success";
    }
    catch (Exception ex) {
        return $"Error: {ex.Message}";
    }
}

 

 

 

 

References

Azure Functions
https://functions.azure.com

Azure Functions Pricing
https://azure.microsoft.com/en-us/pricing/details/functions/

Azure and SendGrid
https://docs.microsoft.com/en-us/azure/sendgrid-dotnet-how-to-send-email

Azure Functions and SendGrid
https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-sendgrid