π Problem
Managed exports never create any Cost Management exports. The config_ConfigureExports pipeline runs, reaches the export creation activity, and fails with:
{"error":{"code":"Unauthorized","message":"The user does not have authorization to perform
'Microsoft.Authorization/roleAssignments/write' action on specified storage account, please use a
storage account with sufficient permissions. If the permissions have changed recently then retry
after some time."}}
Cost Management needs the caller to be able to write role assignments on the destination storage account, because the export is created with a system-assigned identity ("identity": { "type": "systemAssigned" }) for which it must grant access to the destination.
The template is aware of this and passes the Role Based Access Control Administrator role for the Data Factory identity β but the assignment is never actually created, so the Data Factory identity cannot create exports.
π Root cause
The Managed Exports app registers itself with only the DataFactory feature while passing storageRoles:
// src/templates/finops-hub/modules/Microsoft.CostManagement/ManagedExports/app.bicep#L69-L82
module appRegistration '../../fx/hub-app.bicep' = {
name: 'Microsoft.CostManagement.ManagedExports_Register'
params: {
features: [
'DataFactory' // <-- 'Storage' is not declared
]
storageRoles: [
// RBAC Administrator -- used to create Cost Management exports (which require access to grant access)
'f58310d9-a9f6-439a-9e8d-f62e7b41a168'
]
}
}
hub-app.bicep gates all storage role assignments on that feature:
// src/templates/finops-hub/modules/fx/hub-app.bicep#L40
var usesStorage = contains(features, 'Storage')
// src/templates/finops-hub/modules/fx/hub-app.bicep#L287-L298
resource storageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for role in factoryStorageRoles: if (usesDataFactory && usesStorage) {
...
}
]
Because usesStorage is false for this app, the entire loop is skipped and the RBAC Administrator role it passes is silently dropped. Managed Exports is the only app in the template that passes storageRoles at all β and the only one of those that does not declare the Storage feature.
This is visible in the shipped template as well: the role GUID appears exactly once in azuredeploy.json, as the parameter value handed to Microsoft.CostManagement.ManagedExports_Register, immediately next to "features": ["DataFactory"].
π Regression
This worked in v12, where the same role was coupled directly to the managed exports option and assigned without a feature condition:
// v12 -- src/templates/finops-hub/modules/dataFactory.bicep
var storageRbacRoles = union(
[ /* Storage Account Contributor, Storage Blob Data Contributor, Reader */ ],
// Only use User Access Administrator if managed exports are enabled for least privileged access
!enableManagedExports ? [] : [ '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' ]
)
resource factoryIdentityStorageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for role in storageRbacRoles: { ... } // no feature condition
]
The app modularization (#1800) moved roles into a per-app storageRoles parameter and introduced the feature gate; the coupling to enableManagedExports was lost in the process. Present in v13, v14 and current dev.
π£ Repro steps
- Deploy a FinOps hub with
enableManagedExports = true and at least one subscription scope in scopesToMonitor.
- Grant the hub managed identity Cost Management Contributor on that scope, as documented.
- Write to
config/settings.json in the hub storage account (any write) so the config_SettingsUpdated trigger fires.
config_ConfigureExports runs and fails at the ... open month focus export activity with the Unauthorized error above (3 attempts).
- No export is created in Cost Management, so nothing lands in the
msexports container and no data is ingested.
Checking the Data Factory identity afterwards shows four role assignments β Data Factory Contributor on the factory, plus Storage Account Contributor, Storage Blob Data Contributor and Reader on the hub storage account. RBAC Administrator is not among them.
π€ Expected
When enableManagedExports is enabled, the deployment assigns the RBAC Administrator role to the Data Factory identity on the hub storage account, so config_ConfigureExports can create Cost Management exports. This matches the documented behavior in Hub template, which lists the role under the assignments made "as part of the deployment", noting only that it is "not applied when enableManagedExports is disabled".
π§ Environment
- FinOps hub version: current
dev (15.0-dev); also present in v13 and v14. Last known good: v12.
- Billing account type: reproduced on a subscription scope, which is documented as supported for managed exports (MCA billing accounts/profiles are explicitly not supported).
- Cost Management export: FocusCost
Reproduced on two independent deployments with different shapes (with and without Data Explorer, public networking), with the same result.
π‘ Suggested fix
Declare the Storage feature on the Managed Exports app registration so the role assignment loop runs:
features: [
'DataFactory'
'Storage'
]
βΉοΈ Additional context
Two related observations found while investigating, both minor β happy to split them into their own issues if preferred:
-
The role table in the hub template docs still lists User Access Administrator. That GUID no longer appears anywhere in the shipped template since it was replaced by RBAC Administrator, so the documented role cannot be found by anyone looking for it.
-
In the same pipeline run, the Save Scopes activity of config_ConfigureExports fails when the settings file contains exactly one scope:
The variable 'scopesArray' of type 'Array' cannot be initialized or updated with value of type 'Object'.
The settings file serializes a single scope as an object rather than an array, because sorting a one-element collection in PowerShell yields a scalar. This is non-blocking β the following Save Scopes as Array activity recovers and the scope reaches export creation β but it shows up as a failed activity on every run with a single scope and makes troubleshooting harder.
π Problem
Managed exports never create any Cost Management exports. The
config_ConfigureExportspipeline runs, reaches the export creation activity, and fails with:Cost Management needs the caller to be able to write role assignments on the destination storage account, because the export is created with a system-assigned identity (
"identity": { "type": "systemAssigned" }) for which it must grant access to the destination.The template is aware of this and passes the Role Based Access Control Administrator role for the Data Factory identity β but the assignment is never actually created, so the Data Factory identity cannot create exports.
π Root cause
The Managed Exports app registers itself with only the
DataFactoryfeature while passingstorageRoles:hub-app.bicepgates all storage role assignments on that feature:Because
usesStorageisfalsefor this app, the entire loop is skipped and the RBAC Administrator role it passes is silently dropped. Managed Exports is the only app in the template that passesstorageRolesat all β and the only one of those that does not declare theStoragefeature.This is visible in the shipped template as well: the role GUID appears exactly once in
azuredeploy.json, as the parameter value handed toMicrosoft.CostManagement.ManagedExports_Register, immediately next to"features": ["DataFactory"].π Regression
This worked in v12, where the same role was coupled directly to the managed exports option and assigned without a feature condition:
The app modularization (#1800) moved roles into a per-app
storageRolesparameter and introduced the feature gate; the coupling toenableManagedExportswas lost in the process. Present in v13, v14 and currentdev.π£ Repro steps
enableManagedExports = trueand at least one subscription scope inscopesToMonitor.config/settings.jsonin the hub storage account (any write) so theconfig_SettingsUpdatedtrigger fires.config_ConfigureExportsruns and fails at the... open month focus exportactivity with theUnauthorizederror above (3 attempts).msexportscontainer and no data is ingested.Checking the Data Factory identity afterwards shows four role assignments β Data Factory Contributor on the factory, plus Storage Account Contributor, Storage Blob Data Contributor and Reader on the hub storage account. RBAC Administrator is not among them.
π€ Expected
When
enableManagedExportsis enabled, the deployment assigns the RBAC Administrator role to the Data Factory identity on the hub storage account, soconfig_ConfigureExportscan create Cost Management exports. This matches the documented behavior in Hub template, which lists the role under the assignments made "as part of the deployment", noting only that it is "not applied when enableManagedExports is disabled".π§ Environment
dev(15.0-dev); also present in v13 and v14. Last known good: v12.Reproduced on two independent deployments with different shapes (with and without Data Explorer, public networking), with the same result.
π‘ Suggested fix
Declare the
Storagefeature on the Managed Exports app registration so the role assignment loop runs:βΉοΈ Additional context
Two related observations found while investigating, both minor β happy to split them into their own issues if preferred:
The role table in the hub template docs still lists User Access Administrator. That GUID no longer appears anywhere in the shipped template since it was replaced by RBAC Administrator, so the documented role cannot be found by anyone looking for it.
In the same pipeline run, the
Save Scopesactivity ofconfig_ConfigureExportsfails when the settings file contains exactly one scope:The settings file serializes a single scope as an object rather than an array, because sorting a one-element collection in PowerShell yields a scalar. This is non-blocking β the following
Save Scopes as Arrayactivity recovers and the scope reaches export creation β but it shows up as a failed activity on every run with a single scope and makes troubleshooting harder.