In lessons 3.13.1 through 3.13.3 the Function App ran locally. Now it goes to Azure: as Bicep in the repository, delivered with azd, with a managed identity for the host's storage and for Application Insights, without a connection string with a key. After that you measure what serverless promises and what it costs: the cold start and the price per execution. At the end comes the decision whether the Kladde's import stays a worker in Container Apps or runs as Functions, and then everything is torn down again.
Costs of this lesson: A Flex Consumption plan without always ready instances costs money only while functions run, and has a free
allowance per month; on top of that come a storage account, a Log Analytics workspace and Application Insights by data volume. Check the
current prices in the pricing calculator before you begin. An always ready instance costs money even
while it waits; switch it on only for the measurement. The lesson ends with azd down --purge.
The Function App as Bicep
The file infra/functions.bicep creates everything the app needs. Learn describes the same shape in its example for Flex Consumption with
managed identities; here it is tailored to the Kladde:
// 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
What matters here:
- The plan is
FlexConsumptionwith the nameFC1, always on Linux (reserved: true,kind: 'functionapp,linux'). functionAppConfigis the part that exists only for Flex Consumption: where the app's package is delivered (a container in the storage account, read with the identity), how the app scales (at most 40 instances, 2,048 MB each) and which runtime runs (dotnet-isolated, version10).- No key. The storage account allows no access with an account key (
allowSharedKeyAccess: false); the host signs in with the user-assigned identity (AzureWebJobsStorage__credentialand__clientId), and Application Insights accepts telemetry only with Entra sign-in (DisableLocalAuth,APPLICATIONINSIGHTS_AUTHENTICATION_STRING). The connection string of Application Insights contains no key that grants access; it only says where to send. - The roles are assigned by the template itself, with names from
guid()as in lesson 3.3.1: Blob Data Owner, Queue and Table Data Contributor on the storage (Durable Functions with the Azure Storage provider needs queues and tables), Monitoring Metrics Publisher on Application Insights. For Service Bus from lesson 3.13.2, an assignment of Azure Service Bus Data Receiver on the Kladde's namespace is added.
az bicep build --file infra/functions.bicep compiles the file without a warning. Before delivering, check with what-if, as in lesson 3.2.4, and
remember that what-if doesn't prove the deployment will succeed.