Breno Fatureto
30/3/2022

Deploying F# Azure Functions

Photo by Venti Views on Unsplash

Introduction

The plethora of web infrastructure services nowadays can make it hard for you to pick the best offering, specially when considering ease of deploying, maintenance effort and infrastructure costs.

Azure Functions are a serverless computing platform provided by Microsoft. With Azure Functions, you can easily create and deploy web apps that run upon a specified trigger, such as a timer or an HTTP request. Since it’s a serverless platform, you do not have to worry about system administration or package management.

F# is the .NET ecosystem functional language. Its type checking features provide a great developer experience and assure you that your program will run. The fact F# runs on .NET means that you don’t have to give up a great package ecosystem. You can have your cake and eat it too!


In the previous post, we saw how to initialize an Azure Function project, and we wrote a few functions in F#. In this post, we will write an HTTP-triggered Azure Function in F# and deploy it.

Prerequisites

Create an Azure account

This tutorial will require you to have an Azure account. You can register at the Azure portal. If it’s the first time you’re registering, you might be eligible for free credits. Otherwise, executing the example in this tutorial might incur in a cost of a few cents.

Disclaimer: We are not responsible for any charges that may incur from following this tutorial.

.NET 6

We start by installing the .NET development platform on the local machine. The Azure Functions runtime host version 4 requires .NET 6, which is the most recent version as of the time of writing (Dec/2021).

The easiest way to get started is with the dotnet-install.sh script.

curl -L https://dot.net/v1/dotnet-install.sh -o dotnet-install.shchmod +x dotnet-install
./dotnet-install.sh -c 6.0

You can also check the .NET download page for more instructions.

Azure Function Core Tools

We also need to install the Azure Functions Core Tools locally. The project’s GitHub repo provides instructions for a variety of platforms. Make sure to install version 4. Here is the line to install using NPM.

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

If you’re on Arch Linux, there is also an AUR package.

Azure CLI

Microsoft provides a command-line utility that allows you to manage and interact with Azure resources.

The easiest way to get started on Linux is with the official installation script.

curl -L https://aka.ms/InstallAzureCli | bash

Take a look at the official documentation for instructions for other platforms.

Initializing a project

We use the Azure Functions Core Tools to create the project structure.

func init MyFunctionProj

We get a menu showing runtime options. For this project, pick 1 for dotnet

Select a number for worker runtime:

1. dotnet
2. dotnet (isolated process)
3. node4. python
5. powershell
6. custom
Choose option: 1
dotnet

The tool creates a new directory called MyFunctionProj. We need to make a few changes to this original template though. Since the template is originally for C# projects, we need to change the extension of the project file. Also, due to the way the F# compiler handles inclusion directives, we also need to peek inside the project file and change the criteria for copying some project configuration files to the output directory.

First, we have to rename MyFunctionProj.csproj to MyFunctionProj.fsproj. We, then have to manually alter this file so that host.json and local.settings.json are correctly copied to the output build directory. To accomplish that, we have to rename the Update parameter to Include. We also add a rule so that local.settings.json isn't pushed over when you publish your function. Your MyFunctionProj.fsproj file should look like the following.

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="local.settings.json"
Condition="Exists('local.settings.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

Creating an HTTP triggered Azure Function

In the previous post, we’ve seen how to write some simple Azure Functions and how to run them locally. Now we use the dotnet templates to get a more complex HTTP-triggered function which can receive parameters and act on them.

We will write a Celsius to Fahrenheit unit converter API. The user will pass a temperature in Celsius as a query string and receive a text response containing the equivalent value in Fahrenheit.

namespace UnitConverter

open System
open System.IO
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging

module UnitConverter =    
let celsiusToFahrenheit c = (c * 9/5) + 32    

[<FunctionName("UnitConverter")>]    
let run ([<HttpTrigger(AuthorizationLevel.Function, "get",
"post", Route = null)>]req: HttpRequest) (log: ILogger) =        
async {            
log.LogInformation("F# HTTP trigger function processed a request.")  // 1            
let celsiusOpt =                                                    
// 2                
if req.Query.ContainsKey("celsius") then                    
Some(int req.Query.["celsius"].[0])                
else                    
None  
         
let response =                                                      
// 3                
match celsiusOpt with                
| Some c -> celsiusToFahrenheit c |> string                
| None -> ""    
       
return OkObjectResult(response) :> IActionResult                    
// 4        
} |> Async.StartAsTask


Let’s go over this step by step.

The celsiusToFahrenheit function does the actual value conversion. Next, we specify the function to be ran whenever the HTTP trigger activates. In this case, the function is called run and is decorated with the [<FunctionName("UnitConverter")>] attribute. This specifies the actual name of the Azure Function; that is, the name of the entity in the Azure Functions runtime.

The body of the function runs as an async task. In F#, we do this by enclosing the block in an async computation expression. While not used here, this syntax allows us to easily run other async computations inside the block seamlessly.

First (//1) we log a message to notify that the function received a request. After that (//2) we try to fetch the parameter in the query string. We transform that into an Optional value, which may or may not contain a value. With this we can signal whether a parameter has been passed or not.

Finally, (//3), we format the response based on the parameter by converting the value, if present, or just by returning an empty string, if no parameter is passed by the user. We then return the response enclosed in an action result (//4).

Running locally

Before running, we have to include a compile directive in the fsproj file. Just before the </ItemGroup> line, place the following.

<Compile Include="UnitConverter.fs" />

Let’s run our function locally to check whether it does what we expect it to. First, build your project with dotnet build. Now, in order to run the function locally, run func host start.

You should see some output in the terminal.

Azure Functions Core ToolsCore Tools Version:       4.0.3971 Commit hash:
d0775d487c93ebd49e9c1166d5c3c01f3c76eaaf  (64-bit)
Function Runtime Version: 4.0.1.16815

[2022-01-03T18:41:07.514Z] Csproj not found in
/home/breno/Sync/datarisk/azure-function/UnitConverter/bin/output directory tree. Skipping user secrets file configuration.

Functions:        

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

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

Now, that our function is running, let’s try to hit its HTTP endpoint to see what happens. Let’s try to convert 25 °C to Fahrenheit through our API.

curl http://localhost:7071/api/UnitConverter?celsius=25

You should get a response 77 back from the curl command. There should also be a message in the function's log.

[2022-01-03T18:42:24.324Z] F# HTTP trigger function processed a request.
[2022-01-03T18:42:24.325Z] Executed 'UnitConverter' (Succeeded, Id=<ID>, Duration=3ms)

Setting up Azure CLI

Logging in

Make sure you have a Microsoft account for the next step. You also want to link your account with the Azure Portal. You can do so at this link. Make sure you can access the Azure Portal with your account.

Before deploying the Azure Function, we have to set up Azure CLI tool. We start by logging in with az login.

$ az login
The default 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`.

At this point, the CLI tool should open a web browser, allowing you to log in to your Azure account. After that’s done, some JSON output will be printed out.

[  
{    
"cloudName": "AzureCloud",    
"homeTenantId": ...,    
"id": ...,    
"isDefault": true,    
"managedByTenants": [],    
"name": ...,    
"state": "Enabled",    
"tenantId": ...,    
"user": {      
"name": ...,      
"type": "user"    
}  
}
]

Creating a resource group

Next we need to create a resource group, which is the way that Azure groups related resources.

az group create --name myResourceGroup --location westus

You should get a JSON response like the following.

{  

"id": "/subscriptions/<ID>/resourceGroups/myResourceGroup",  "location": "westus",  
"managedBy": null,  
"name": "myResourceGroup",  
"properties": {    
"provisioningState": "Succeeded"  
},  
"tags": null,  
"type": "Microsoft.Resources/resourceGroups"
}

Creating an Azure Storage Account

Now we create a storage account under the resource group we’ve just created. We will use this storage account to store information regarding our Azure function.

Note that you need to provide a unique name to the storage resource. I recommend that you get a unique name by combining a prefix name of your choice along with a randomly generated number. We can do this by setting a new variable in the shell and referencing it whenever we need the storage account name.

storageName=mystorageaccount$RANDOM

If you need to get the storage name, run echo $storageName. Now we create the storage resource with the newly generated name.

az storage account create \  
--name $storageName \  
--location westus \  
--resource-group myResourceGroup \  
--sku Standard_LRS

If the command succeeds, you should get a long response with the details of your newly-created storage account.

{  
"accessTier": "Hot",  
"allowBlobPublicAccess": true,  
"allowCrossTenantReplication": null,
...

Creating an Azure Function

Finally, it’s time to create the Azure Function itself. Note that we associate it with the resource group and storage account that we created in the previous steps. We also use version 4 of the runtime. This is the most recent version as of the time of writing (early 2022), and supports .NET 6.

We also require a unique name here.

functionName=myazurefunction$RANDOM

Now run the az functionapp create command.

az functionapp create \  
--name $functionName \  
--storage-account $storageName \  
--consumption-plan-location westus \  
--resource-group myResourceGroup \  
--functions-version 4

If everything goes well, you should get another long response with the details of your Azure functions.

{  
"availabilityState": "Normal",  
"clientAffinityEnabled": false,  
"clientCertEnabled": false,  
"clientCertExclusionPaths": null,  
...

Note that this command also gives you a website URL for your Azure function. We will use this later on when we deploy our function in order to test it. You can find under the URL in the defaultHostName field.

{  
...  
"defaultHostName": "YOURAZUREFUNCTIONNAME.azurewebsites.net",
...
}

Deploying with Azure CLI

Now that we’ve written our function, ran it locally, and created all the Azure resources, we are ready to deploy it.

func azure functionapp publish $functionName

This command will build your function and try to deploy it. If everything goes well you should get some output that looks like this.

$ func azure functionapp publish $functionName
Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.  

Determining projects to restore...  
All projects are up-to-date for restore.  
UnitConverter -> /home/UnitConverter/bin/publish/UnitConverter.dll

Build succeeded.    
0 Warning(s)    
0 Error(s)

Time Elapsed 00:00:05.39

Getting site publishing info...
Creating archive for current directory...
Uploading 4.65 MB
[###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in AZUREFUNCTIONNAME:    
UnitConverter - [httpTrigger]        
Invoke url:
https://AZUREFUNCTIONNAME.azurewebsites.net/api/unitconverter?code=ID

Success! Now let’s test out our freshly deployed function. Take the URL given in the Invoke url line in the output of the previous command. For example, if your invoke URL is https://function123.azurewebsites.net/api/unitconverter?code=code123, run the following:

curl https://function123.azurewebsites.net/api/unitconverter?code=code123\&celsius=25

This will run the same command as we ran when we were testing the function locally. You should get the response 77.

Conclusion

In this tutorial you’ve learned how to:

  • create an Azure Function project in F#,
  • create Azure resources with the az tool, and
  • deploy an Azure Function to the cloud.

You can now try to tweak your function to receive more complex queries, or try to play around with other Azure Function triggers, like the Timer Trigger.


At Datarisk, we have a dedicated development team keen on custom data processing solutions and technologies. We are ready to help you create a robust solution. Visit our site to see how we solve real world problems.

Thanks to Eduardo Bellani, Juarez Sampaio, Vinicius Gajo, and Guilherme Marz for reviewing and providing feedback for this article.


Further Reading

Mais artigos da Datarisk

Ver todos os artigos
© Copyright 2017 - 2022 | Datarisk.io | Todos os direitos reservados