Freigeben über


Using F# to write serverless Azure functions

Guest post by Thomas Denney Microsoft Student Partner at the University of Oxford.

Over the last few years cloud providers have shifted from offering Infrastructure-as-a-Service to Platform-as-a-Service, helping to reduce the knowledge required to maintain web apps and increase abstraction over maintenance and scaling. Function-as-a-Service, also known as serverless, is the next stage of abstraction, as developers instead write functions that respond to HTTP requests, execute after an event, or execute on a specified schedule. Resource management for the function is handled by the cloud provider, allowing for automatic scaling across multiple regions, and the developer isn't charged for when they aren't executing, which can reduce operating costs for a cloud service.

As each execution of the function could happen inside a different VM, on a different physical service, or even in a different continent. Therefore serverless encourages inherently stateless functions, which makes functional languages like F# a great tool for developing Azure Functions.

Getting started

To create an initial F# Azure Function you'll need to begin by creating a new Function App, which is a service that manages the resources for multiple related functions. When you create a Function App you'll have to option to either use the Consumption Plan or the App Service plan. This article will only use the Consumption Plan, which bills by execution time, but the App Service plan may be more appropriate if you have existing VMs or want more predictable billing. At the time of writing Microsoft doesn't bill for the first 1,000,000 executions of an Azure Function per month so it is unlikely you'll be charged anything whilst creating the REST APIs described in this article. The function app only takes a couple of minutes to deploy, at which point you're ready to start writing F# functions.

 function_creation

Once your function app is deployed you can create a new function. The current UI only shows the option to create a C# or Node.js function, but if you click Create your own custom function you'll have the option to create a new F# function. Select the GenericWebHook-FSharp function and give it a name.

 default_function

The default function responds to a POST request with a simple greeting. To test this you can either run the function from within the Azure portal, or run the following curl command:

 curl -"Content-Type: application/json" -d "{\"first\": \"azure\", \"last\": \"function\"}" https://<appname>.azurewebsites.net/api/<functionname>?code=<code>

To get a key (code) to use with your app, click on Keys on the right hand side in the Azure portal, or click Manage under your function name on the left hand side.

To make development and deployment of functions easier, we'll next set up local deployment from git, although other deployment approaches such as Visual Studio Team Services, GitHub, or OneDrive can be used:

  1. Go to the Function app settings
  2. Click Configure continuous integration
  3. Under Deployments, click Setup
  4. Click Configure Required Source under Choose Source
  5. Select Local Git Repository
  6. Setup basic authentication

Once you've configured the deployment, you can run the following to make a local copy of the repository:

 git clone https://<git_username>@<app_name>.scm.azurewebsites.net:443/<app_name>.git

It is possible to test functions locally using the Azure Functions CLI, although it is currently in beta and does not support non-Windows platforms. Alternatively, you could locally develop precompiled functions.

Creating a minimal JSON API

Before creating something more interesting, it is worth adapting the default API to produce a minimal JSON API. Begin by creating a new directory hello in your git repository, and in that folder create a new file function.json. This file is used to specify the route for the API, the HTTP methods that can be used, and the authentication level required. Our new API will not require an authentication key (hence why anonymous is used for the authLevel):

 {
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"],
      "route": "hello/{name:alpha}/{age:int}",
      "authLevel": "anonymous"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

Routes are specified using route constraints, which allow you to specify the desired type of each of the URL parameters. Here we specify a string parameter name that only uses uppercase or lowercase letters, and an integer age parameter. These parameters are then mirrored exactly by parameters to the function.

Next, create a new file run.jsx in the basic directory:

 #r "System.Net.Http"
#r "System.Runtime.Serialization.dll"

open System.Net
open System.Net.Http
open System.Runtime.Serialization

// Using DataContract and DataMember annotations allow us to specify the output
// field name; by default it will be Greeting@
[<DataContract>]
type Greeting = {
    [<field: DataMember(Name="greeting")>]
    Greeting: string
}

// This function is executed by the Azure Function environment. Microsoft
// recommends that this function uses async tasks, although it is not strictly
// necessary
let Run(req: HttpRequestMessage, name: string, age: int, log: TraceWriter) =
    async {
        try
            return req.CreateResponse(HttpStatusCode.OK,
                { Greeting = sprintf "Hello %s, aged %d!" name age })
        with _ ->
            return req.CreateResponse(HttpStatusCode.BadRequest)
    } |> Async.StartAsTask

You can perform requests to this API via the following curl command:

 curl https://<appname>.azurewebsites.net/api/basic/<name>/<age>

Parsing query parameters

Whilst embedding parameters for a GET API into the URI isn't uncommon, they more commonly appear as parameters at the end of the URL (i.e. in the form ?key=value&...). Typically an API will have optional and required parameters, and the required parameters may need to be parsed (e.g. converting the string representation to an integer). We can take advantage of the functional idioms in F# to do this succinctly:

 exception MissingParam of string
exception MalformedParam of string

// This is a helper function that we use for converting the sequence of key
// value pairs into a map (F#'s immutable equivalent of a dictionary)
let paramsMap x = x |> Seq.map (fun (KeyValue (k,v)) -> (k,v)) |> Map.ofSeq

// If the parameter has been specified in the request, then we attempt to parse
// it using the function f, which should take a string and return a value of the
// desired type. For example, when we just want to get the string later on we
// specify the identity function id, but when we want to parse the string as an
// integer we instead use the System.Int32.Parse function. If the function f
// throws an exception we then throw the malformed parameter exception (defined
// above) so that an appropriate error message is returned by the API.
// Alternatively, if the parameter wasn't specified we throw a missing parameter
// exception
let reqParam f k m =
    match Map.tryFind k m with
    | Some x -> try f x
                with
                | _ -> raise (MalformedParam k)
    | None -> raise (MissingParam k)

// For optional parameters we instead pass a default value, d, which is returned
// in the case that the parameter is missing. 'd' and 'f x' should have the same
// type
let optParam f k d m =
    match Map.tryFind k m with
    | Some x -> try f x
                with
                | _ -> raise (MalformedParam x)
    | None -> d

let Run(req: HttpRequestMessage, log: TraceWriter) =
    async {
        try
            let ps = req.GetQueryNameValuePairs() |> paramsMap
            // With the helper functions defined as above it is now
            // straightforward to retreive required and optional parameters
            let text = reqParam id "text" ps
            let count = optParam System.Int32.Parse "count" 1 ps
            // The final output of this API is a greeting, repeated a specified
            // number of times
            let g = sprintf "Hello %s. " text |> String.replicate count
            return req.CreateResponse(HttpStatusCode.OK, { Greeting = g })
        with
            // We can also provide meaningful responses in the case of errors
            | MissingParam(p) ->
                return req.CreateResponse(HttpStatusCode.BadRequest, sprintf "Missing param '%s'" p)
            | MalformedParam(p) ->
                return req.CreateResponse(HttpStatusCode.BadRequest, sprintf "Malformed param '%s'" p)
            | _                 -> return req.CreateResponse(HttpStatusCode.BadRequest)
    } |> Async.StartAsTask

Scheduled functions

Another simple use case for Azure Functions is to run a piece of code on a regular schedule. Begin by creating a new directory timed and create a new file function.json inside it:

 {
    "bindings": [
        {
            "schedule": "0 */5 * * * *",
            "name": "myTimer",
            "type": "timerTrigger",
            "direction": "in"
        }
    ]
}

Defining our function, inside run.fsx is straightforward:

 let Run(myTimer: TimerInfo, log: TraceWriter ) =
    DateTime.Now.ToLongTimeString() |> sprintf "Executed at %s" |> log.Info

This gets the current date, formats a string with it, and then logs that string every time the function is executed. After deploying the function with git you can view the logs in Azure (you can also force the function to run by click Run):

logs

Conclusion

This article has shown how to develop Azure functions in F# that run on a schedule and respond to HTTP requests, thus providing the basis for a larger web app. As well as F#, it is possible to "bring your own runtime", although this isn't yet officially supported, or more common web backend languages, such as C# or Node.js.

Additional resources for developing Azure functions: