Create serverless applications (Function App) - acsgt/MSLearnAzure GitHub Wiki

https://docs.microsoft.com/en-us/learn/paths/create-serverless-applications/

0. Choose the best Azure service to automate your business processes

TODO2 ss

1. Create serverless logic with Azure Functions

JavaScript:

module.exports = async function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');

const name = (req.query.name || (req.body && req.body.name));
const responseMessage = name
    ? "Hello, " + name + ". This HTTP triggered function executed successfully."
    : "This HTTP triggered function executed successfully. Pass a name on the query string or in the request body for a personalized response.";

context.res = {
    // status: 200, /* Defaults to 200 */
    body: responseMessage
};

}

Powershell:

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."

# Interact with query parameters or the body of the request.
$name = $Request.Query.Name
if (-not $name) {
    $name = $Request.Body.Name
}

if ($name) {
    $status = [HttpStatusCode]::OK
    $body = "Hello $name"
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "Please pass a name on the query string or in the request body."
}

# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

function.json:

Javascript:

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

Powershell:

{ "bindings": [ { "authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "Request", "methods": [ "get", "post" ] }, { "type": "http", "direction": "out", "name": "res" } ], }

cUrl:

curl --header "Content-Type: application/json" --header "x-functions-key: <your-function-key>" --request POST --data "{\"name\": \"Azure Function\"}" <your-https-url>

Add business logic to the function

Javascript:

module.exports = function (context, req) {
    context.log('Drive Gear Temperature Service triggered');
    if (req.body && req.body.readings) {
        req.body.readings.forEach(function(reading) {

            if(reading.temperature<=25) {
                reading.status = 'OK';
            } else if (reading.temperature<=50) {
                reading.status = 'CAUTION';
            } else {
                reading.status = 'DANGER'
            }
            context.log('Reading is ' + reading.status);
        });

        context.res = {
            // status: 200, /* Defaults to 200 */
            body: {
                "readings": req.body.readings
            }
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please send an array of readings in the request body"
        };
    }
    context.done();
};

PowerShell:

using namespace System.Net

param($Request, $TriggerMetadata)

Write-Host "Drive Gear Temperature Service triggered"

$readings = $Request.Body.Readings
if ($readings) {
    foreach ($reading in $readings) {
        if ($reading.temperature -le 25) {
            $reading.Status = "OK"
        }
        elseif ($reading.temperature -le 50) {
            $reading.Status = "CAUTION"
        }
        else {
            $reading.Status = "DANGER"
        }

        Write-Host "Reading is $($reading.Status)"
    }

    $status = [HttpStatusCode]::OK
    $body = $readings
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "Please send an array of readings in the request body"
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

2. Execute an Azure Function with triggers

TimeTrigger

C# TimeTrigger:

using System;

public static void Run(TimerInfo myTimer, ILogger log)
{
    log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
}

Function.json:

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

HttpTrigger

#r "Newtonsoft.Json"

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

public static async Task<IActionResult> 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;

    string responseMessage = string.IsNullOrEmpty(name)
        ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";

            return new OkObjectResult(responseMessage);
}

Example:

.../api/HttpTriggerCSharp1?name=Jesse

function.json:

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

BlobTrigger:

public static void Run(Stream myBlob, string name, ILogger log)
{
    log.LogInformation($"C# Blob trigger function Processed blob\n Name:{name} \n Size: {myBlob.Length} Bytes");
}

function.json:

{
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "samples-workitems/{name}",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

3. Chain Azure Functions together using input and output bindings

Read data with input bindings

Node.JS:

module.exports = function (context, req) {

    var bookmark = context.bindings.bookmark

    if(bookmark){
        context.res = {
            body: { "url": bookmark.url },
            headers: { 'Content-Type': 'application/json' }
        };
    }
    else {
        context.res = {
            status: 404,
            body : "No bookmarks found",
            headers: { 'Content-Type': 'application/json' }
        };
    }
    context.done();
};

PowerShell:

using namespace System.Net

param($Request, $bookmark, $TriggerMetadata)

if ($bookmark) {
    $status = [HttpStatusCode]::OK
    $body = @{ url = $bookmark.url }
    ContentType = "application/json"
}
else {
    $status = [HttpStatusCode]::NotFound
    $body = "No bookmarks found"
    ContentType = "text/plain"
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

function.json:

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "bookmark",
      "direction": "in",
      "type": "cosmosDB",
      "databaseName": "func-io-learn-db",
      "collectionName": "Bookmarks",
      "connectionStringSetting": "your-database_DOCUMENTDB",
      "id": "{id}",
      "partitionKey": "{id}"
    }
  ]
}

Write data with output bindings

module.exports = function (context, req) {

    var bookmark = context.bindings.bookmark
    if(bookmark){
            context.res = {
            status: 422,
            body : "Bookmark already exists.",
            headers: { 'Content-Type': 'application/json' }
        };
    }
    else {
        // Create a JSON string of our bookmark.
        var bookmarkString = JSON.stringify({ 
            id: req.body.id,
            url: req.body.url
        });

        // Write this bookmark to our database.
        context.bindings.newbookmark = bookmarkString;

        // Push this bookmark onto our queue for further processing.
        context.bindings.newmessage = bookmarkString;

        // Tell the user all is well.
        context.res = {
            status: 200,
            body : "bookmark added!",
            headers: { 'Content-Type': 'application/json' }
        };
    }
    context.done();
};

function.json:

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "bookmark",
      "direction": "in",
      "type": "cosmosDB",
      "connectionStringSetting": "gtazf3u5dbaccount_DOCUMENTDB",
      "databaseName": "func-io-learn-db",
      "collectionName": "Bookmarks",
      "id": "id",
      "partitionKey": "/id"
    },
    {
      "name": "newbookmark",
      "direction": "out",
      "type": "cosmosDB",
      "connectionStringSetting": "gtazf3u5dbaccount_DOCUMENTDB",
      "databaseName": "func-io-learn-db",
      "collectionName": "Bookmarks",
      "partitionKey": "/id"
    },
    {
      "name": "newmessage",
      "direction": "out",
      "type": "queue",
      "connection": "AzureWebJobsStorage",
      "queueName": "bookmarks-post-process"
    }
  ]
}

4. Create a long-running serverless workflow with Durable Functions

npm install durable-functions

HttpStart (Durable Functions HTTP starter):

const df = require("durable-functions");

module.exports = async function (context, req) {
    const client = df.getClient(context);
    const instanceId = await client.startNew(req.params.functionName, undefined, req.body);

    context.log(`Started orchestration with ID = '${instanceId}'.`);

    return client.createCheckStatusResponse(context.bindingData.req, instanceId);
};

OrchFunction (Durable Functions orchestrator):

const df = require("durable-functions");

module.exports = df.orchestrator(function* (context) {
    const outputs = [];

    /*
    * We will call the approval activity with a reject and an approved to simulate both
    */

    outputs.push(yield context.df.callActivity("Approval", "Approved"));
    outputs.push(yield context.df.callActivity("Approval", "Rejected"));

    return outputs;
});

Approval (Durable Functions activity)

module.exports = async function (context) {
    return `Your project design proposal has been -  ${context.bindings.name}!`;
};

Add a durable timer to manage a long-running task

npm install typescript
npm install moment

Escalation (Durable Functions activity)

module.exports = async function (context) {
    return `ESCALATION : You have not approved the project design proposal - reassigning to your Manager!  ${context.bindings.name}!`;
};

OrchFunction - updated (Durable Functions orchestrator):

const moment = require("moment");
module.exports = df.orchestrator(function* (context) {
    const outputs = [];
    const deadline = moment.utc(context.df.currentUtcDateTime).add(20, "s");
    const activityTask = context.df.waitForExternalEvent("Approval");
    const timeoutTask = context.df.createTimer(deadline.toDate());

    const winner = yield context.df.Task.any([activityTask, timeoutTask]);
    if (winner === activityTask) {
        outputs.push(yield context.df.callActivity("Approval", "Approved"));
    }
    else
    {
        outputs.push(yield context.df.callActivity("Escalation", "Head of department"));
    }

    if (!timeoutTask.isCompleted) {
        // All pending timers must be complete or canceled before the function exits.
        timeoutTask.cancel();
    }

    return outputs;
});

5. Develop, test, and publish Azure Functions by using Azure Functions Core Tools

func init
func new
func start
code .

simple-interest:

module.exports = async function(context, req) {
    // Try to grab principal, rate, and term from the query string and
    // parse them as numbers
    const principal = parseFloat(req.query.principal);
    const rate = parseFloat(req.query.rate);
    const term = parseFloat(req.query.term);

    if ([principal, rate, term].some(isNaN)) {
      // If any empty or non-numeric values, return a 400 response with an
      // error message
      context.res = {
        status: 400,
        body: "Please supply principal, rate and term in the query string"
      };
    } else {
      // Otherwise set the response body to the product of the three values
      context.res = { body: principal * rate * term };
    }
};

Test:

func start &> ~/output.txt &
curl "http://localhost:7071/api/simple-interest?principal=5000&rate=.035&term=36" -w "\n"
pkill func

Publish:

$az login --tenant grzegorztrochimiuk.onmicrosoft.com
$az functionapp list --query "[].name"
func azure functionapp publish gt-azf-4u4

Azure CLI:

RESOURCEGROUP=[sandbox resource group]
STORAGEACCT=learnstorage$(openssl rand -hex 5)
FUNCTIONAPP=learnfunctions$(openssl rand -hex 5)

az storage account create \
  --resource-group "$RESOURCEGROUP" \
  --name "$STORAGEACCT" \
  --kind StorageV2 \
  --location centralus

az functionapp create \
  --resource-group "$RESOURCEGROUP" \
  --name "$FUNCTIONAPP" \
  --storage-account "$STORAGEACCT" \
  --runtime node \
  --consumption-plan-location centralus \
  --functions-version 2

6. Develop, test, and deploy an Azure Function with Visual Studio

Publish:

az functionapp deployment source config-zip \
-g <resource-group> \
-n <function-app-name> \
--src <zip-file>

WatchInfo.cs

// Retrieve the model id from the query string
string model = req.Query["model"];

// If the user specified a model id, find the details of the model of watch
if (model != null)
{
    // Use dummy data for this example
    dynamic watchinfo = new { Manufacturer = "abc", CaseType = "Solid", Bezel = "Titanium", Dial = "Roman", CaseFinish = "Silver", Jewels = 15 };

    return (ActionResult)new OkObjectResult($"Watch Details: {watchinfo.Manufacturer}, {watchinfo.CaseType}, {watchinfo.Bezel}, {watchinfo.Dial},{watchinfo.CaseFinish}, {watchinfo.Jewels}");
}
return new BadRequestObjectResult("Please provide a watch model in the query string");

Test xUnit Project:

using System;
using Xunit;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.Logging.Abstractions;

namespace WatchFunctionsTests
{
    public class WatchFunctionUnitTests
    {
        [Fact]
        public void TestWatchFunctionSuccess()
        {
            var queryStringValue = "abc";
            var request = new DefaultHttpRequest(new DefaultHttpContext())
            {
                Query = new QueryCollection
                (
                    new System.Collections.Generic.Dictionary<string, StringValues>()
                    {
                        { "model", queryStringValue }
                    }
                )
            };

            var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");

            var response = WatchPortalFunction.WatchInfo.Run(request, logger);
            response.Wait();

            // Check that the response is an "OK" response
            Assert.IsAssignableFrom<OkObjectResult>(response.Result);

            // Check that the contents of the response are the expected contents
            var result = (OkObjectResult)response.Result;
            dynamic watchinfo = new { Manufacturer = "abc", CaseType = "Solid", Bezel = "Titanium", Dial = "Roman", CaseFinish = "Silver", Jewels = 15 };
            string watchInfo = $"Watch Details: {watchinfo.Manufacturer}, {watchinfo.CaseType}, {watchinfo.Bezel}, {watchinfo.Dial}, {watchinfo.CaseFinish}, {watchinfo.Jewels}";
            Assert.Equal(watchInfo, result.Value);
        }

        [Fact]
        public void TestWatchFunctionFailureNoQueryString()
        {
            var request = new DefaultHttpRequest(new DefaultHttpContext());
            var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");

            var response = WatchPortalFunction.WatchInfo.Run(request, logger);
            response.Wait();

            // Check that the response is an "Bad" response
            Assert.IsAssignableFrom<BadRequestObjectResult>(response.Result);

            // Check that the contents of the response are the expected contents
            var result = (BadRequestObjectResult)response.Result;
            Assert.Equal("Please provide a watch model in the query string", result.Value);
        }

        [Fact]
        public void TestWatchFunctionFailureNoModel()
        {
            var queryStringValue = "abc";
            var request = new DefaultHttpRequest(new DefaultHttpContext())
            {
                Query = new QueryCollection
                (
                    new System.Collections.Generic.Dictionary<string, StringValues>()
                    {
                        { "not-model", queryStringValue }
                    }
                )
            };

            var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");

            var response = WatchPortalFunction.WatchInfo.Run(request, logger);
            response.Wait();

            // Check that the response is an "Bad" response
            Assert.IsAssignableFrom<BadRequestObjectResult>(response.Result);

            // Check that the contents of the response are the expected contents
            var result = (BadRequestObjectResult)response.Result;
            Assert.Equal("Please provide a watch model in the query string", result.Value);
        }
    }
}

7. Monitor GitHub events by using a webhook with Azure Functions

Header

Request URL: https://testwh123456.azurewebsites.net/api/HttpTrigger1?code=aUjXIpqdJ0ZHPQuB0SzFegxGJu0nAXmsQBnmkCpJ6RYxleRaoxJ8cQ%3D%3D
Request method: POST
Accept: */*
content-type: application/json
User-Agent: GitHub-Hookshot/16496cb
X-GitHub-Delivery: 9ed46280-6ab3-11e9-8a19-f1a14922a239
X-GitHub-Event: gollum
X-GitHub-Hook-ID: 312141005
X-GitHub-Hook-Installation-Target-ID: 394459163
X-GitHub-Hook-Installation-Target-Type: repository

Body

"pages": [
    {
        "page_name": "Home",
        "title": "Home",
        "summary": null,
        "action": "edited",
        "sha": "04d012c5f92a95ae3f7721173bf9f2b1b35ea22f",
        "html_url": "https://github.com/.../wiki/Home"
    }
],
"repository" : {
    "id": 176302421,
    "node_id": "MDEwOlJlcG9zaXRvcnkxNzYzMDI0MjE=",
    "name": "tieredstorage",
    ...
},
"sender" : {
    ...
}

HttpTrigger:

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    const name = (req.query.name || (req.body && req.body.name));
    const responseMessage = name
        ? "Hello, " + name + ". This HTTP triggered function executed successfully."
        : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.";

    if (req.body.pages[0].title){
        context.res = {
            body: "Page is " + req.body.pages[0].title + ", Action is " + req.body.pages[0].action + ", Event Type is " + req.headers['x-github-event']
        };
     }
     else {
        context.res = {
            status: 400,
            body: ("Invalid payload for Wiki event")
        };
    }
}

webhook + secret:

const Crypto = require('crypto');

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    const hmac = Crypto.createHmac("sha1", "<default key>");
    const signature = hmac.update(JSON.stringify(req.body)).digest('hex');
    const shaSignature =  `sha1=${signature}`;
    const gitHubSignature = req.headers['x-hub-signature'];

    if (!shaSignature.localeCompare(gitHubSignature)) {
        if (req.body.pages[0].title) {
            context.res = {
                body: "Page is " + req.body.pages[0].title + ", Action is " + req.body.pages[0].action + ", Event Type is " + req.headers['x-github-event']
            };
        }
        else {
            context.res = {
                status: 400,
                body: ("Invalid payload for Wiki event")
            }
        }
    }
    else {
        context.res = {
            status: 401,
            body: "Signatures don't match"
        };
    }
};

8. Enable automatic updates in a web application using Azure Functions and SignalR Service

https://github.com/MicrosoftDocs/mslearn-advocates.azure-functions-and-signalr

git clone https://github.com/MicrosoftDocs/mslearn-advocates.azure-functions-and-signalr.git serverless-demo

Create a Storage account

export STORAGE_ACCOUNT_NAME=mslsigrstorage$(openssl rand -hex 5)
echo "Storage Account Name: $STORAGE_ACCOUNT_NAME"
az storage account create \
  --name $STORAGE_ACCOUNT_NAME \
  --resource-group [sandbox resource group name] \
  --kind StorageV2 \
  --sku Standard_LRS

Create an Azure Cosmos DB account

az cosmosdb create  \
  --name msl-sigr-cosmos-$(openssl rand -hex 5) \
  --resource-group [sandbox resource group name]

Update local settings (local.settings.json)

STORAGE_CONNECTION_STRING=$(az storage account show-connection-string \
--name $(az storage account list \
  --resource-group [sandbox resource group name] \
  --query [0].name -o tsv) \
--resource-group [sandbox resource group name] \
--query "connectionString" -o tsv)

COSMOSDB_ACCOUNT_NAME=$(az cosmosdb list \
    --resource-group [sandbox resource group name] \
    --query [0].name -o tsv)

COSMOSDB_CONNECTION_STRING=$(az cosmosdb list-connection-strings  \
  --name $COSMOSDB_ACCOUNT_NAME \
  --resource-group [sandbox resource group name] \
  --query "connectionStrings[?description=='Primary SQL Connection String'].connectionString" -o tsv)

COSMOSDB_MASTER_KEY=$(az cosmosdb list-keys \
--name $COSMOSDB_ACCOUNT_NAME \
--resource-group [sandbox resource group name] \
--query primaryMasterKey -o tsv)

printf "\n\nReplace <STORAGE_CONNECTION_STRING> with:\n$STORAGE_CONNECTION_STRING\n\nReplace <COSMOSDB_CONNECTION_STRING> with:\n$COSMOSDB_CONNECTION_STRING\n\nReplace <COSMOSDB_MASTER_KEY> with:\n$COSMOSDB_MASTER_KEY\n\n"

Test:

npm install
npm start
npm run update-data

Adding SignalR:

SIGNALR_SERVICE_NAME=msl-sigr-signalr$(openssl rand -hex 5)
az signalr create \
  --name $SIGNALR_SERVICE_NAME \
  --resource-group learn-b462221e-bf9f-4c8a-a390-7bc82312ab79 \
  --sku Free_DS2 \
  --unit-count 1
az resource update \
  --resource-type Microsoft.SignalRService/SignalR \
  --name $SIGNALR_SERVICE_NAME \
  --resource-group learn-b462221e-bf9f-4c8a-a390-7bc82312ab79 \
  --set properties.features[flag=ServiceMode].value=Serverless

Get connection string:

SIGNALR_CONNECTION_STRING=$(az signalr key list \
  --name $(az signalr list \
    --resource-group learn-b462221e-bf9f-4c8a-a390-7bc82312ab79 \
    --query [0].name -o tsv) \
  --resource-group learn-b462221e-bf9f-4c8a-a390-7bc82312ab79 \
  --query primaryConnectionString -o tsv)
printf "\n\nReplace <SIGNALR_CONNECTION_STRING> with:\n$SIGNALR_CONNECTION_STRING\n\n"

List of subscriptions:

az account list --query "[?name=='Concierge Subscription'].tenantId" -o tsv

9. Expose multiple Azure Function apps as a consistent API by using Azure API Management

10. Build serverless apps with Go

⚠️ **GitHub.com Fallback** ⚠️