ARM scripting ServerLess SignalR with Upstream Setting to Function App

Viewed 187

I have a 'ServerLess' SignalR resource with an Upstream Setting to talk to a backing Function App. All is working. The problem is how to automate the creation of these resources since there appears to be a circular reference between the two.

The SignalR Upstream Setting needs the 'signalr_extension' App Key from the Function App to include in the Upstream URL template. Conversely, the Function App needs an 'AzureSignalRConnectionString' app setting. Hence the circular reference.

To complicate things, it appears that the 'signalr_extension' App Key is only created when you deploy SignalR-triggered functions to the Function App. Can a randomized App Key be set 'manually' in the Function App at ARM Template creation time instead, then used in building the SignalR resource Upstream Setting?

I've tried to 'dump' a working Function App as an output in an ARM template using listkeys/listsecrets, but the Host/App Keys do not appear to be exposed.

Is it possible to script (using ARM or Azure CLI) the creation of these resources?

2 Answers

After some experimentation, I think I've solved this. Here are the steps:

  1. in the ARM template, create the Function App and SignalR. Make the Function App depend (dependsOn) on Signal. Inject the SignalR AzureSignalRConnectionString appsetting into the Function App.

  2. Deploy the function code to the Function App.

  3. Use the Azure CLI to extract the 'signalr_extension':

    az functionapp keys list --name <function_app_name> --resource-group <resource_group>

  4. Take the resulting systemKeys/signalr_extension value and put it into another Azire CLI command:

    az signalr upstream update --name <signalr_name> --resource-group <resource_group> --template url-template="https://<function_app_name>.azurewebsites.net/runtime/webhooks/signalr?code=<code from step 3"

At least this can all be scripted in to a DevOps pipeline.

Using Azure Bicep, I managed to get it working in one step:
Only tested for a function app v4 dotnet6 on windows.

  • Create a storage account
  • Create an app service plan (windows)
  • Create the function app without the app settings
  • Create the signalr_extension system key
  • Create signalR service with the signalr_extension system key
  • Deploy the function app appsettings with the AzureSignalRConnectionString connectionstring.

main.bicep:

param location string = resourceGroup().location

param storageName string
param appServicePlanName string
param functionAppName string
param signalRName string

// Create storage
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: storageName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}

// Create serverless app service plan
resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: 'Y1'
    tier: 'Dynamic'
    size: 'Y1'
    family: 'Y'
    capacity: 0
  }
  properties: {
    reserved: false
  }
}

// Create the function app without the app settings
resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  properties: {
    serverFarmId: appServicePlan.id
    clientAffinityEnabled: false
    clientCertEnabled: false
    httpsOnly: true
    siteConfig: {
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
      cors: {
        allowedOrigins: [
          'https://portal.azure.com'
        ]
        supportCredentials: false
      }
    }
  }
}

// Create the signalr key
var signalrKeyName = 'signalr_extension'
resource signalRKey 'Microsoft.Web/sites/host/systemkeys@2021-03-01' = {
  name: '${functionApp.name}/default/${signalrKeyName}'
  properties: {
    name: signalrKeyName
  }
}

// Create the signalR service and inject the signalr_extension key
resource signalR 'Microsoft.SignalRService/signalR@2022-02-01' = {
  name: signalRName
  location: location
  dependsOn: [
    signalRKey
  ]
  sku: {
    name: 'Free_F1'
    tier: 'Free'
    capacity: 1
  }
  properties: {
    features: [
      {
        flag: 'ServiceMode'
        value: 'Serverless'
      }
      {
        flag: 'EnableConnectivityLogs'
        value: 'true'
      }
    ]
    cors: {
      allowedOrigins: [
        '*'
      ]
    }
    tls: {
      clientCertEnabled: false
    }
    upstream: {
      templates: [
        {
          hubPattern: '*'
          eventPattern: '*'
          categoryPattern: '*'
          auth: {
            type: 'None'
          }
          urlTemplate: 'https://${functionApp.name}.azurewebsites.net/runtime/webhooks/signalr?code=${listKeys(resourceId('Microsoft.Web/sites/host', functionApp.name, 'default'), '2022-03-01').systemkeys.signalr_extension}'
        }
      ]
    }
  }
}

// Deploy the app settings at the end with the storage and signalR connectionstring
resource functionAppAppSettings 'Microsoft.Web/sites/config@2020-09-01' = {
  name: '${functionApp.name}/appsettings'
  properties: {
    FUNCTIONS_EXTENSION_VERSION: '~4'
    FUNCTIONS_WORKER_RUNTIME: 'dotnet'
    WEBSITE_RUN_FROM_PACKAGE: '1'
    AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${listKeys(storage.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net;'
    WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${listKeys(storage.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net;'
    WEBSITE_CONTENTSHARE: functionApp.name
    AzureSignalRConnectionString: listkeys(signalR.id, signalR.apiVersion).primaryConnectionString
  }
}
Related