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
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.