In den Lektionen 3.13.1 bis 3.13.3 lief die Function App lokal. Jetzt kommt sie nach Azure: als Bicep im Repository, ausgeliefert mit azd, mit verwalteter Identität für den Speicher des Hosts und für Application Insights, ohne Verbindungszeichenfolge mit Schlüssel. Danach messen Sie, was Serverless verspricht und kostet: den Kaltstart und den Preis je Ausführung. Am Ende steht die Entscheidung, ob der Import der Kladde als Worker in Container Apps bleibt oder als Functions läuft, und danach wird alles wieder abgebaut.

Kosten dieser Lektion: Ein Flex-Consumption-Plan kostet ohne immer bereite Instanzen nur, wenn Funktionen laufen, und hat ein kostenloses Kontingent im Monat; dazu kommen ein Speicherkonto, ein Log-Analytics-Arbeitsbereich und Application Insights nach Datenmenge. Sehen Sie die aktuellen Preise im Preisrechner nach, bevor Sie beginnen. Eine immer bereite Instanz kostet auch, wenn sie wartet; schalten Sie sie nur für die Messung ein. Die Lektion endet mit azd down --purge.

Die Function App als Bicep

Die Datei infra/functions.bicep legt alles an, was die App braucht. Learn beschreibt dieselbe Form in seinem Beispiel für Flex Consumption mit verwalteten Identitäten; hier ist sie für die Kladde zugeschnitten:

bicep
// functions.bicep: the Kladde's Function App in a Flex Consumption plan, without a single key
@description('Region of all resources.')
param location string = resourceGroup().location

@description('Short name of the environment, part of every resource name.')
@allowed(['dev', 'test', 'prod'])
param environmentName string = 'dev'

@description('Memory of one instance in MB.')
@allowed([512, 2048, 4096])
param instanceMemoryMB int = 2048

@description('Always ready instances of the HTTP group; every one is paid while it waits.')
@minValue(0)
param alwaysReadyHttp int = 0

var token = toLower(uniqueString(resourceGroup().id, environmentName))
var appName = 'func-kladde-${environmentName}-${token}'
var deploymentContainer = 'app-package'

// Role definitions from the Azure built-in roles.
var storageBlobDataOwner = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
var storageQueueDataContributor = '974c5e8b-45b9-4653-ba55-5f855dd0fb88'
var storageTableDataContributor = '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'
var monitoringMetricsPublisher = '3913510d-42f4-4e42-8a64-420c390055eb'

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'id-kladde-func-${environmentName}'
  location: location
}

resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'stkladdefn${token}'
  location: location
  kind: 'StorageV2'
  sku: { name: 'Standard_LRS' }
  properties: {
    allowSharedKeyAccess: false
    allowBlobPublicAccess: false
    minimumTlsVersion: 'TLS1_2'
  }
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
  parent: storage
  name: 'default'
}

resource packages 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
  parent: blobService
  name: deploymentContainer
}

resource workspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: 'log-kladde-func-${environmentName}'
  location: location
  properties: {
    sku: { name: 'PerGB2018' }
    retentionInDays: 30
  }
}

resource insights 'Microsoft.Insights/components@2020-02-02' = {
  name: 'appi-kladde-func-${environmentName}'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: workspace.id
    DisableLocalAuth: true
  }
}

resource plan 'Microsoft.Web/serverfarms@2024-04-01' = {
  name: 'plan-kladde-func-${environmentName}'
  location: location
  kind: 'functionapp'
  sku: {
    tier: 'FlexConsumption'
    name: 'FC1'
  }
  properties: {
    reserved: true
  }
}

resource app 'Microsoft.Web/sites@2024-04-01' = {
  name: appName
  location: location
  kind: 'functionapp,linux'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: { '${identity.id}': {} }
  }
  properties: {
    serverFarmId: plan.id
    httpsOnly: true
    siteConfig: { minTlsVersion: '1.2' }
    functionAppConfig: {
      deployment: {
        storage: {
          type: 'blobContainer'
          value: '${storage.properties.primaryEndpoints.blob}${deploymentContainer}'
          authentication: {
            type: 'UserAssignedIdentity'
            userAssignedIdentityResourceId: identity.id
          }
        }
      }
      scaleAndConcurrency: {
        maximumInstanceCount: 40
        instanceMemoryMB: instanceMemoryMB
        alwaysReady: alwaysReadyHttp > 0 ? [ { name: 'http', instanceCount: alwaysReadyHttp } ] : []
      }
      runtime: {
        name: 'dotnet-isolated'
        version: '10'
      }
    }
  }

  resource settings 'config' = {
    name: 'appsettings'
    properties: {
      AzureWebJobsStorage__accountName: storage.name
      AzureWebJobsStorage__credential: 'managedidentity'
      AzureWebJobsStorage__clientId: identity.properties.clientId
      APPLICATIONINSIGHTS_CONNECTION_STRING: insights.properties.ConnectionString
      APPLICATIONINSIGHTS_AUTHENTICATION_STRING: 'ClientId=${identity.properties.clientId};Authorization=AAD'
    }
  }
}

var storageRoles = [storageBlobDataOwner, storageQueueDataContributor, storageTableDataContributor]

resource storageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for role in storageRoles: {
  name: guid(storage.id, identity.id, role)
  scope: storage
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', role)
    principalId: identity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}]

resource metricsRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(insights.id, identity.id, monitoringMetricsPublisher)
  scope: insights
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', monitoringMetricsPublisher)
    principalId: identity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}

output appName string = app.name
output defaultHostName string = app.properties.defaultHostName

Was hier zählt:

  • Der Plan ist FlexConsumption mit dem Namen FC1, immer unter Linux (reserved: true, kind: 'functionapp,linux').
  • functionAppConfig ist der Teil, den es nur bei Flex Consumption gibt: wohin das Paket der App ausgeliefert wird (ein Container im Speicherkonto, gelesen mit der Identität), wie die App skaliert (höchstens 40 Instanzen, je 2.048 MB) und welche Laufzeit läuft (dotnet-isolated, Version 10).
  • Kein Schlüssel. Das Speicherkonto erlaubt keinen Zugriff mit Kontoschlüssel (allowSharedKeyAccess: false); der Host meldet sich mit der benutzerzugewiesenen Identität an (AzureWebJobsStorage__credential und __clientId), Application Insights nimmt Telemetrie nur mit Entra-Anmeldung an (DisableLocalAuth, APPLICATIONINSIGHTS_AUTHENTICATION_STRING). Die Verbindungszeichenfolge von Application Insights enthält keinen Schlüssel, der Zugriff gewährt; sie sagt nur, wohin.
  • Die Rollen vergibt die Vorlage selbst, mit Namen aus guid() wie in Lektion 3.3.1: Blob Data Owner, Queue und Table Data Contributor auf dem Speicher (Durable Functions mit dem Anbieter Azure Storage braucht Warteschlangen und Tabellen), Monitoring Metrics Publisher auf Application Insights. Für Service Bus aus Lektion 3.13.2 kommt eine Zuweisung von Azure Service Bus Data Receiver auf dem Namespace der Kladde dazu.

az bicep build --file infra/functions.bicep übersetzt die Datei ohne Warnung. Prüfen Sie vor dem Ausliefern mit What-if, wie in Lektion 3.2.4, und denken Sie daran, dass What-if nicht beweist, dass die Bereitstellung gelingt.