- .NET 8.0
- Azure CLI, version 2.39.0 or higher
- SED
- Azure Functions Core Tools
Clone or download this repo locally.
git clone https://github.com/mspnp/serverless-reference-implementation.git
cd serverless-reference-implementation/src
These instructions target Linux-based systems. For Windows machine, install Windows Subsystem for Linux and choose a Linux distribution. Make sure to install the .Net Core libraries specific to your environment. For Linux or Windows Subsystem for Linux, choose this installation for your distribution .
Export the following environment variables:
export LOCATION=<location>
export RESOURCEGROUP_BASE_NAME=<resource-group>
export APPNAME=<functionapp-name> # Cannot be more than 6 characters
export APP_INSIGHTS_LOCATION=<application-insights-location>
export COSMOSDB_DATABASE_NAME=${APPNAME}-db
export COSMOSDB_DATABASE_COL=${APPNAME}-col
export RESOURCEGROUP=${RESOURCEGROUP_BASE_NAME}-${LOCATION}
Note: This reference implementation uses Application Insights, an Azure resource that might not be available in all regions. Ensure to select a region for
APP_INSIGHTS_LOCATION
that supports this resource, preferably the same as or nearest region toLOCATION
for network performance and cost benefits.
Login to Azure CLI and select your subscription.
az login
az account list --output table
az account set --subscription <your-subscription-id>
Create a resource group.
az group create -n $RESOURCEGROUP -l $LOCATION
Deploy Azure resources.
az deployment group create \
-g ${RESOURCEGROUP} \
--template-file azuredeploy-backend-functionapps.bicep \
--parameters appName=${APPNAME} \
appInsightsLocation=${APP_INSIGHTS_LOCATION} \
cosmosDatabaseName=${COSMOSDB_DATABASE_NAME} \
cosmosDatabaseCollection=${COSMOSDB_DATABASE_COL} \
userObjectId=$(az ad signed-in-user show --query id --output tsv)
Create Cosmos DB database and collection. This resource is one of the most expensive, in order to take care the cost in the current reference implementation the container has throughput set to autoscale with a maximum 5000 throughput unites, this would be enough for the current example. In production, the configuration need to be appropriate to process the telemetry.
# Get the Cosmos DB account name from the deployment output
export COSMOSDB_DATABASE_ACCOUNT=$(az deployment group show \
-g ${RESOURCEGROUP} \
-n azuredeploy-backend-functionapps \
--query properties.outputs.cosmosDatabaseAccount.value \
-o tsv)
# Create the Cosmos DB database
az cosmosdb sql database create \
-g $RESOURCEGROUP \
-a $COSMOSDB_DATABASE_ACCOUNT \
-n $COSMOSDB_DATABASE_NAME
# Create the collection
az cosmosdb sql container create \
-g $RESOURCEGROUP \
-a $COSMOSDB_DATABASE_ACCOUNT \
-d $COSMOSDB_DATABASE_NAME \
-n $COSMOSDB_DATABASE_COL \
--partition-key-path /id --max-throughput 5000
Deploy the drone status function
## Get the functiona app name from the deployment output
export DRONE_STATUS_FUNCTION_APP_NAME=$(az deployment group show \
-g ${RESOURCEGROUP} \
-n azuredeploy-backend-functionapps \
--query properties.outputs.droneStatusFunctionAppName.value \
-o tsv)
# Deploy the function to the function app
cd DroneStatus/dotnet/DroneStatusFunctionApp/
func azure functionapp publish ${DRONE_STATUS_FUNCTION_APP_NAME} --dotnet-isolated
cd ../../..
Deploy the drone telemetry function
## Get the functiona app name from the deployment output
export DRONE_TELEMETRY_FUNCTION_APP_NAME=$(az deployment group show \
-g ${RESOURCEGROUP} \
-n azuredeploy-backend-functionapps \
--query properties.outputs.droneTelemetryFunctionAppName.value \
-o tsv)
# Deploy the function to the function app
cd ./DroneTelemetry/DroneTelemetryFunctionApp/
func azure functionapp publish ${DRONE_TELEMETRY_FUNCTION_APP_NAME} --dotnet-isolated
cd ./../..
Get the function key for the DroneStatus function.
- Open the Azure portal
- Navigate to the resource group and open the DroneStatus function app
- Click Fuctions and then select GetStatusFunction.
- Under Function Keys, copy the default value
Deploy API Management
export FUNCTIONAPP_KEY=<function-key-from-the-previous-step>
export FUNCTIONAPP_URL="https://$(az functionapp show -g ${RESOURCEGROUP} -n ${DRONE_STATUS_FUNCTION_APP_NAME} --query defaultHostName -o tsv)/api"
# This process takes more than one hour to complete. If an error occurs due to propagation time, running the script again should resolve the issue.
az deployment group create \
-g ${RESOURCEGROUP} \
--template-file azuredeploy-apim.bicep \
--parameters functionAppNameV1=${DRONE_STATUS_FUNCTION_APP_NAME} \
appName=${APPNAME} \
functionAppCodeV1=${FUNCTIONAPP_KEY} \
functionAppUrlV1=${FUNCTIONAPP_URL}
# list Event Hub namespace name(s)
export EH_NAMESPACE=$(az eventhubs namespace list \
-g $RESOURCEGROUP \
--query '[].name' --output tsv)
export FUllY_QUALIFIED_NAMESPACE="${EH_NAMESPACE}.servicebus.windows.net"
export EVENT_HUB_NAME=$APPNAME-eh
export SIMULATOR_PROJECT_PATH=DroneSimulator/Serverless.Simulator/Serverless.Simulator.csproj
dotnet build $SIMULATOR_PROJECT_PATH
dotnet run --project $SIMULATOR_PROJECT_PATH
The simulator sends data to Event Hubs, which triggers the drone telemetry function app. You can verify the function app is working by viewing the logs in the Azure portal. Navigate to the dronetelemetry
function app resource, select RawTelemetryFunction, expand the Monitor tab, and click on any of the logs.
Also, you can see the database, which will be populated by the drone status. To see the data, in Azure Cosmos DB account -> Networking, you need to add your current IP.
This step creates a new app registration for the API in Microsoft Entra ID, and enables OIDC authentication in the function app.
If you're planning on using the tenant associated to your Azure subscription you can retrieve it.
export TENANT_ID=$(az account show --query tenantId --output tsv)
If you're planning on using a different tenant instead, log in to that tenant. You will need to log in the subscription after creating the application to continue the instructions.
export TENANT_ID=<your tenant id>
az login --tenant $TENANT_ID --allow-no-subscriptions
# Specify the new app name
export API_APP_NAME=<app name>
# Collect information about your tenant
export FEDERATION_METADATA_URL="https://login.microsoftonline.com/$TENANT_ID/FederationMetadata/2007-06/FederationMetadata.xml"
export ISSUER_URL=$(curl $FEDERATION_METADATA_URL --silent | sed -n 's/.*entityID="\([^"]*\).*/\1/p')
export TENANT_DOMAIN=$(az ad signed-in-user show --query 'userPrincipalName' | cut -d '@' -f 2 | sed 's/\"//')
# Create the application registration, defining a new application role and requesting access to read a user using the Graph API
export API_APP_ID=$(az ad app create --display-name $API_APP_NAME --enable-access-token-issuance true \
--is-fallback-public-client false --identifier-uris "http://$TENANT_DOMAIN/$API_APP_NAME" \
--app-roles ' [ { "allowedMemberTypes": [ "User" ], "description":"Access to device status", "displayName":"Get Device Status", "isEnabled":true, "value":"GetStatus" }]' \
--required-resource-accesses ' [ { "resourceAppId": "00000003-0000-0000-c000-000000000000", "resourceAccess": [ { "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", "type": "Scope" } ] }]' \
--query appId --output tsv)
export API_APP_OBJECTID=$(az ad app show --id $API_APP_ID --query id --output tsv)
export IDENTIFIER_URI=$(az ad app show --id $API_APP_ID --query identifierUris[0] -o tsv)
# Generate API Scope
uuid=$(uuidgen)
PATCH_BODY_SCOPE="{api:{oauth2PermissionScopes:[{'adminConsentDescription': 'Status.Read','adminConsentDisplayName': 'Status.Read', 'id': '${uuid}','isEnabled': true,'type': 'User','userConsentDescription': null,'userConsentDisplayName': null,'value': 'Status.Read'}]}}"
az rest --method PATCH --uri https://graph.microsoft.com/v1.0/applications/$API_APP_OBJECTID --headers 'Content-Type=application/json' --body "$PATCH_BODY_SCOPE"
# Create a service principal for the registered application
az ad sp create --id $API_APP_ID
az ad sp update --id $API_APP_ID --set tags='["WindowsAzureActiveDirectoryIntegratedApp"]'
Log back into your subscription if you've used a different tenant.
az login
az account set --subscription <your-subscription-id>
az extension add --name authV2
az webapp auth config-version upgrade --resource-group $RESOURCEGROUP --name $DRONE_STATUS_FUNCTION_APP_NAME
az webapp auth microsoft update --resource-group $RESOURCEGROUP --name $DRONE_STATUS_FUNCTION_APP_NAME --client-id $API_APP_ID --allowed-audiences $IDENTIFIER_URI --issuer $ISSUER_URL
az webapp auth update --resource-group $RESOURCEGROUP --name $DRONE_STATUS_FUNCTION_APP_NAME --enabled --action Return401
This is required for the admin user who will need to be authenticated to use the Azure Function.
-
In the Azure Portal, navigate to your Microsoft Entra ID tenant.
-
Click on Enterprise Applications and then search and select the Drone Status application name.
-
Click Users and groups.
-
Click Add user.
-
Click Users and groups.
-
Select a user, and click Select.
Note: If you define more than one App role in the manifest, you can select the user's role. In this case, there is only one role, so the option is grayed out.
-
Click Assign.
Follow the instructions here to deploy the SPA. Make sure to follow these instructions in the same session and keep the session open to make variables available to the next step.
Next, update the policies in the API Management gateway. This update adds the CDN endpoint URL as an allowed origin for the CORS configuration, and enables authentication for tokens issued for the registered application for the API.
export API_MANAGEMENT_SERVICE=$(az deployment group show \
--resource-group ${RESOURCEGROUP} \
--name azuredeploy-apim \
--query properties.outputs.apimGatewayServiceName.value \
--output tsv)
export API_POLICY_ID="$(az resource show --resource-group $RESOURCEGROUP --resource-type Microsoft.ApiManagement/service --name $API_MANAGEMENT_SERVICE --query id --output tsv)/apis/dronedeliveryapiv1/policies/policy"
az resource create --id $API_POLICY_ID \
--properties "{
\"value\": \"<policies><inbound><base /><cors allow-credentials=\\\"true\\\"><allowed-origins><origin>$CLIENT_URL</origin></allowed-origins><allowed-methods><method>GET</method></allowed-methods><allowed-headers><header>*</header></allowed-headers></cors><validate-jwt header-name=\\\"Authorization\\\" failed-validation-httpcode=\\\"401\\\" failed-validation-error-message=\\\"Unauthorized. Access token is missing or invalid.\\\"><openid-config url=\\\"${ISSUER_URL}.well-known/openid-configuration\\\" /><required-claims><claim name=\\\"aud\\\"><value>$IDENTIFIER_URI</value></claim></required-claims></validate-jwt></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>\"
}"
Execute the url $CLIENT_URL
in your browser
Optionally, it is possible to have backend versions side by side of drone status by deploying a v2.