Deploy a TypeScript function app to Flex Consumption without keys

Build an Azure Functions app in TypeScript (programming model v4) and deploy it to Flex Consumption with no storage keys — a user-assigned identity, a Shared Key-disabled account for host storage and the package, three storage roles, identity-based AzureWebJobsStorage and binding connections, functionAppConfig in Bicep (Microsoft.Web 2025-03-01), a v4 HTTP function with a queue output, the package.json main glob, and the zip deploy.

markdownguidev1S6Q1-SFPTstage: buildtrust: officialservice: functionsinterface: iacinterface: sdkinterface: clilanguages: typescriptlanguages: bicepdeploy TypeScript Azure FunctionFlex Consumption BicepfunctionAppConfig BicepMicrosoft.Web/sites@2025-03-01Microsoft.Web/serverfarms FC1keyless function appAzureWebJobsStorage__accountName Bicepidentity-based queue output bindingOrdersQueue__queueServiceUrioutput.storageQueueapp.http v4HttpResponseInitpackage.json main dist/src/functions/*.jsaz functionapp deployment source config-zip--build-remotefunc azure functionapp publishMicrosoft.Web BCP081 2025-05-01

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

Checked with Bicep 0.48.1: Microsoft.Web 2025-05-01, 2026-03-15 and 2026-07-15 are stable in the REST specs but have no Bicep types (BCP081), so this uses 2025-03-01. Don't create this app with az functionapp create: it always writes a key-based connection string (reference/functions/errors.md).

1. Infrastructure (main.bicep)

@description('Function app name: globally unique, and keep it under 32 characters (host ID).')
@maxLength(32)
param appName string

@description('Storage account for host storage and the deployment package: 3-24 lowercase letters and numbers.')
param storageName string

param location string = resourceGroup().location

@allowed(['22', '24'])
param nodeVersion string = '22'

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
  name: 'id-${appName}'
  location: location
}

resource storage 'Microsoft.Storage/storageAccounts@2026-06-01' = {
  name: storageName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowSharedKeyAccess: false
    allowBlobPublicAccess: false
    defaultToOAuthAuthentication: true
  }
}

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

resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2026-06-01' = {
  parent: blobService
  name: 'app-package'
}

// Storage Blob Data Owner (host minimum), Storage Table Data Contributor (host diagnostic events),
// Storage Queue Data Contributor (the app's own queue output binding)
var storageRoles = [
  'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
  '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'
  '974c5e8b-45b9-4653-ba55-5f855dd0fb88'
]

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

resource plan 'Microsoft.Web/serverfarms@2025-03-01' = {
  name: 'plan-${appName}'
  location: location
  kind: 'functionapp'
  sku: {
    name: 'FC1'
    tier: 'FlexConsumption'
  }
  properties: {
    reserved: true
  }
}

resource app 'Microsoft.Web/sites@2025-03-01' = {
  name: appName
  location: location
  kind: 'functionapp,linux'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    serverFarmId: plan.id
    httpsOnly: true
    siteConfig: {
      minTlsVersion: '1.2'
      appSettings: [
        {
          name: 'AzureWebJobsStorage__accountName'
          value: storage.name
        }
        {
          name: 'AzureWebJobsStorage__credential'
          value: 'managedidentity'
        }
        {
          name: 'AzureWebJobsStorage__clientId'
          value: identity.properties.clientId
        }
        {
          name: 'OrdersQueue__queueServiceUri'
          value: storage.properties.primaryEndpoints.queue
        }
        {
          name: 'OrdersQueue__credential'
          value: 'managedidentity'
        }
        {
          name: 'OrdersQueue__clientId'
          value: identity.properties.clientId
        }
        {
          name: 'FUNCTIONS_NODE_BLOCK_ON_ENTRY_POINT_ERROR'
          value: 'true'
        }
      ]
    }
    functionAppConfig: {
      deployment: {
        storage: {
          type: 'blobContainer'
          value: '${storage.properties.primaryEndpoints.blob}${deploymentContainer.name}'
          authentication: {
            type: 'UserAssignedIdentity'
            userAssignedIdentityResourceId: identity.id
          }
        }
      }
      scaleAndConcurrency: {
        instanceMemoryMB: 2048
        maximumInstanceCount: 100
      }
      runtime: {
        name: 'node'
        version: nodeVersion
      }
    }
  }
  dependsOn: [
    storageRoleAssignments
  ]
}

output appUrl string = 'https://${app.properties.defaultHostName}'
output identityClientId string = identity.properties.clientId

main.bicepparam:

using './main.bicep'

param appName = 'func-orders-prod'
param storageName = 'stfuncordersprod'

Deploy with az deployment group create -g <rg> -f main.bicep -p main.bicepparam. The app waits on the role assignments (dependsOn), but role propagation can still take minutes: if the first start logs storage 403s, restart the app after 10–30 minutes. No setting holds a key; there's no FUNCTIONS_WORKER_RUNTIME or WEBSITE_RUN_FROM_PACKAGE (Flex rejects them). Add APPLICATIONINSIGHTS_CONNECTION_STRING for telemetry (observability family).

2. The function (src/functions/createOrder.ts)

import { app, HttpRequest, HttpResponseInit, InvocationContext, output } from "@azure/functions";

// Queue "orders" through the identity-based connection "OrdersQueue"
// (app settings OrdersQueue__queueServiceUri, OrdersQueue__credential, OrdersQueue__clientId).
const ordersQueue = output.storageQueue({ queueName: "orders", connection: "OrdersQueue" });

interface Order {
  id: string;
  amount: number;
}

function isOrder(value: unknown): value is Order {
  const v = value as Partial<Order> | null;
  return typeof v?.id === "string" && typeof v?.amount === "number";
}

export async function createOrder(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
  const body: unknown = await request.json().catch(() => undefined);
  if (!isOrder(body)) {
    return { status: 400, jsonBody: { error: "Expected { id: string, amount: number }" } };
  }
  context.extraOutputs.set(ordersQueue, body);
  context.log(`Queued order ${body.id}`);
  return { status: 202, jsonBody: { id: body.id } };
}

app.http("createOrder", {
  methods: ["POST"],
  authLevel: "function",
  route: "orders",
  extraOutputs: [ordersQueue],
  handler: createOrder,
});

package.json needs "main": "dist/src/functions/*.js" (the compiled output, not .ts), @azure/functions 4.x in dependencies, and a build script running tsc. A wrong main is the usual cause of No job functions found. For local runs, local.settings.json holds the same connection names; locally they authenticate as your az login identity, which needs the same roles.

3. Deploy the code

npm ci && npm run build
zip -r app.zip host.json package.json package-lock.json dist
az functionapp deployment source config-zip -g <rg> -n <app> --src app.zip --build-remote true

--build-remote true runs npm install on the platform, so node_modules stays out of the zip. Then call https://<app>.azurewebsites.net/api/orders with a function key (az functionapp function keys list -g <rg> -n <app> --function-name createOrder), and confirm a message reaches the orders queue. Failures: reference/functions/errors.md.

Telemetry for the app: guides/monitor/instrument-node-app-with-opentelemetry.md and reference/monitor/application-insights-errors.md.

Read this document as markdown — the same bytes an agent receives.