Why Flex Consumption fixed a problem Y1 could not
A Linux Consumption function app can only fetch its deployment package from a SAS URL. User-delegation SAS expires in seven days. That leaves one bad option — unless you move to Flex.
I set out to build a small monitoring app on Azure Functions with no stored credentials anywhere. Cosmos and Service Bus both had disableLocalAuth: true, every binding used a managed identity, and the Bicep had exactly one connection string left in it — AzureWebJobsStorage, which I fully intended to delete on the next pass.
Then the deployment package turned out to be the thing that would not let go of its key.
The setup
Y1 Linux Consumption, deployed with az functionapp deployment source config-zip. That command uploads your zip, stores it, and points the app at it with WEBSITE_RUN_FROM_PACKAGE.
On Windows Consumption you can set WEBSITE_RUN_FROM_PACKAGE=1 and the platform manages the package itself. On Linux Consumption that value is not supported. The setting has to be a URL the host can fetch the package from.
So the host needs to authenticate to blob storage. And on Y1, its only way to do that is a SAS token embedded in the URL.
Where it falls apart
There are two kinds of SAS you can hand it.
A user-delegation SAS is signed with Entra credentials rather than the storage account key. It is the right shape — no account key involved. Try to issue one for a year and the CLI stops you:
ERROR: incorrect usage: --expiry should be within 7 days from now
That is not an az quirk. A user-delegation key has a maximum lifetime of seven days by design. So your function app works for a week and then cannot fetch its own code.
An account-key SAS has no such limit. It is also a long-lived credential derived from the storage account key, sitting in an app setting, granting read access to your deployment container until you remember to rotate it. Which is precisely the thing I had spent the afternoon removing everywhere else.
Neither is acceptable. One breaks weekly; the other is the anti-pattern.
Flex Consumption removes the choice
On Flex, the package location is not an app setting with a SAS in it. It is structured configuration on the site resource, and it takes an authentication type:
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
name: 'func-example'
location: location
kind: 'functionapp,linux'
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
functionAppConfig: {
deployment: {
storage: {
type: 'blobContainer'
value: '${storage.properties.primaryEndpoints.blob}deployments'
authentication: {
type: 'SystemAssignedIdentity'
}
}
}
scaleAndConcurrency: {
maximumInstanceCount: 40
instanceMemoryMB: 2048
}
runtime: { name: 'node', version: '22' }
}
}
}
The app authenticates to its own package store as itself. No SAS, no expiry, nothing to rotate.
The plan is FC1 / FlexConsumption:
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'plan-example'
location: location
sku: { name: 'FC1', tier: 'FlexConsumption' }
kind: 'functionapp'
properties: { reserved: true }
}
Note what is not there: no linuxFxVersion. The runtime moves into functionAppConfig.runtime, and setting both is a good way to confuse yourself.
The part that made it worth doing
Once the identity was already fetching the package, the last connection string went too:
{ name: 'AzureWebJobsStorage__accountName', value: storage.name }
The __accountName suffix is how the Functions host knows to reach for the managed identity instead of parsing a connection string. Same pattern as ServiceBusConnection__fullyQualifiedNamespace and CosmosConnection__accountEndpoint.
That needs three role assignments on the storage account, not one:
- Storage Blob Data Owner — the host’s own state, and the package
- Storage Queue Data Contributor
- Storage Table Data Contributor
Blob Data Contributor is not enough for the host’s internal use. That one costs people an hour.
Two things that will catch you
You cannot convert Y1 to FC1 in place. The SKUs are not interchangeable — you delete the app and the plan and redeploy. Which leads directly to the second thing.
Role assignment names are immutable in the ways that matter. The common Bicep pattern is:
name: guid(storage.id, functionApp.id, roleDefinitionId)
Delete and recreate the function app and it gets a new principal ID, but functionApp.id is unchanged — so the GUID is unchanged, so Azure tries to update the existing assignment with a different principal:
RoleAssignmentUpdateNotPermitted: Tenant ID, application ID, principal ID,
and scope are not allowed to be updated.
The instinct is to put principalId in the guid() inputs. That does not compile:
BCP120: This expression is being used in an assignment to the "name" property
of the "Microsoft.Authorization/roleAssignments" type, which requires a value
that can be calculated at the start of the deployment.
Role assignment names must be resolvable before the deployment runs, and a module output is not. So either delete the stale assignments by hand before redeploying, or derive the name from something stable that you also control — the computed app name rather than the resource’s .id.
I deleted five orphaned assignments by hand and moved on.
Was it worth it
For a hobby app, arguably not — an account-key SAS would have worked and nobody would have known.
But the whole point of the build was that a compromised repository or a leaked app setting should not hand anyone access to anything. Leaving one long-lived storage credential in place to serve the deployment package would have been a strange place to stop, and the Y1-to-Flex move took about forty minutes including the role assignment cleanup.
Flex is also where the platform is going. Y1 Linux Consumption is the older shape, and its package-fetch story is a good illustration of why.