Create serverless applications (Function App) - acsgt/MSLearnAzure GitHub Wiki
https://docs.microsoft.com/en-us/learn/paths/create-serverless-applications/
TODO2 ss
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>
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
})
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 * * * * *"
}
]
}
#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"
}
]
}
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"
}
]
}
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}"
}
]
}
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"
}
]
}
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}!`;
};
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;
});
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
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);
}
}
}
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"
};
}
};
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