I am trying to provision N storage accounts, N role assignments (1 per storage account) that grant access to a specific identity, but only conditionally deploy the role assignments. The storage accounts and identity already exist and template logic for them has been working for some time.
If I try to deploy the below template snippet, I hit "the language expression property array index '0' is out of bounds" on "'[concat(parameters('BackupStorageAccountRoleAssignmentsDeployment')[0].AccountName, '/Microsoft.Authorization/', guid(parameters('BackupStorageAccountRoleAssignmentsDeployment')[0].Name))]'" when the input array of role assignments is empty.
I'm already using trick to force a 0 length copy to length = 1 and then guard deployment on a condition. I've tried variations using a default array of size 1, moving my role assignment section into a nested template, and manually unrolling my loop into 4 role assignments. No matter what, I hit the same error. What is wrong with this snippet?
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"RoleAssignments": {
"type": "array"
},
"IdentityName": {
"type": "string"
},
"StorageAccounts": {
"type": "array"
}
},
"variables": {
"IdentityResourceId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('IdentityName'))]"
},
"resources": [
{ // Everything related to role assignments chokes completely, even if I unroll the loop
"dependsOn": [
"[variables('IdentityResourceId')]",
"storageaccountcopy"
],
"copy": {
"name": "RoleAssignmentsCopy",
"count": "[max(length(parameters('BackupStorageAccountRoleAssignmentsDeployment')), 1)]"
},
"condition": "[greater(length(parameters('RoleAssignments')), 0)]",
"type": "Microsoft.Storage/storageAccounts/providers/roleAssignments",
"apiVersion": "2020-04-01-preview",
"name": "[concat(parameters('RoleAssignments')[copyIndex()].AccountName, '/Microsoft.Authorization/', guid(parameters('RoleAssignments')[copyIndex()].Name))]",
"properties": {
"roleDefinitionId": "[parameters('RoleAssignments')[copyIndex()].RoleDefinitionId]",
"principalId": "[if(and(empty(parameters('RoleAssignments')[copyIndex()].ResourceId), not(empty(parameters('RoleAssignments')[copyIndex()].PrincipalId))),
parameters('RoleAssignments')[copyIndex()].PrincipalId,
reference(parameters('RoleAssignments')[copyIndex()].ResourceId, '2018-11-30').PrincipalId)]",
"scope": "[parameters('RoleAssignments')[copyIndex()].Scope]",
"principalType": "[parameters('RoleAssignments')[copyIndex()].PrincipalType]"
}
},
{ // Everthing related to storage account provisioning works fine, this was existing code
"condition": "[and(greater(length(parameters('StorageAccounts')), 0), not(parameters('SkipStorageAccountProvisioning')), equals(parameters('StorageAccountProvisioningDefault'), 'true'))]",
"copy": {
"name": "storageaccountcopy",
"count": "[length(parameters('StorageAccounts'))]"
},
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[parameters('StorageAccounts')[copyIndex()].AccountName]",
"location": "[resourceGroup().location]",
"sku": {
"name": "[if(equals(parameters('StorageAccounts')[copyIndex()].AccountTypeOverride, parameters('StorageAccountTypeOverrideDefault')), parameters('StorageAccounts')[copyIndex()].AccountType, parameters('StorageAccounts')[copyIndex()].AccountTypeOverride)]",
"tier": "Standard"
},
"kind": "[parameters('StorageAccounts')[copyIndex()].AccountKind]",
"properties": {
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [],
"ipRules": [],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": true,
"encryption": {
"services": {
"file": {
"keyType": "Account",
"enabled": true
},
"blob": {
"keyType": "Account",
"enabled": true
}
},
"keySource": "Microsoft.Storage"
}
}
}
]
}
EDIT:
Per feeback I tried moving the snippet into a nested template. Fails with the exact same error, The template resource '[concat(parameters('RoleAssignments')[copyIndex()].AccountName, '/Microsoft.Authorization/', guid(parameters('BackupStorageAccountRoleAssignments')[copyIndex()].Name))]' at line '1' and column '837' is not valid: The language expression property array index '0' is out of bounds..
{
"dependsOn": [
"[variables('IdentityResourceId')]"
],
"type": "Microsoft.Resources/deployments",
"apiVersion": "2020-10-01",
"name": "RoleAssignmentsDeployment",
"properties": {
"mode": "Incremental",
"expressionEvaluationOptions": {
"scope": "inner"
},
"parameters": {
"RoleAssignments": {
"value": "[parameters('RoleAssignmentsDeployment')]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"RoleAssignments": {
"type": "array"
}
},
"resources": [
{
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', parameters('RoleAssignments')[copyIndex()].AccountName)]"
],
"copy": {
"name": "RoleAssignmentsDeploymentCopy",
"count": "[max(length(parameters('RoleAssignments')), 1)]"
},
"condition": "[greater(length(parameters('RoleAssignments')), 0)]",
"type": "Microsoft.Storage/storageAccounts/providers/roleAssignments",
"apiVersion": "2020-10-01",
"name": "[concat(parameters('RoleAssignments')[copyIndex()].AccountName, '/Microsoft.Authorization/', guid(parameters('RoleAssignments')[copyIndex()].Name))]",
"properties": {
"scope": "[parameters('RoleAssignments')[copyIndex()].Scope]",
"principalType": "[parameters('RoleAssignments')[copyIndex()].PrincipalType]",
"roleDefinitionId": "[parameters('RoleAssignments')[copyIndex()].RoleDefinitionId]",
"principalId": "[if(and(empty(parameters('RoleAssignments')[copyIndex()].ResourceId), not(empty(parameters('RoleAssignments')[copyIndex()].PrincipalId))),
parameters('RoleAssignments')[copyIndex()].PrincipalId,
reference(parameters('RoleAssignments')[copyIndex()].ResourceId, '2020-10-01').PrincipalId)]"
}
}
]
}
}
}
So there are a few ways to solve this. I had some difficulty getting Miq's suggestion to work the way I want. I did find another workaround though; use a variable in the template to ensure that the input array is ALWAYs populated
Notes:
Regular "defaultValue" property on the parameter won't work, default values won't be applied if an empty array is passed. You need to use some if block to compute a single-element array as a default value for a no-op deployment
ARM expands templates at "compile" time. So you get the "0 is out of bounds" error based on replacement values for the template, not when the actual deployment starts. The problem was I was trying to use a zero length copy AND I was referencing that copyindex()
within special ARM functions/items like reference
and dependsOn
.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"RoleAssignments": {
"type": "array"
},
"IdentityName": {
"type": "string"
},
"StorageAccounts": {
"type": "array"
},
"Enabled": {
"type: "bool"
}
},
"variables": {
"IdentityResourceId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('IdentityName'))]",
"ComputedRoleAssignments": "[if(greater(length(parameters('RoleAssignments')), 0),
parameters('RoleAssignments'),
createArray(createObject('AccountName', 'dummy', 'Name', 'dummy', 'ResourceId', 'dummy', 'RoleDefinitionId', 'dummy', 'PrincipalType', 'dummy', 'Scope', 'dummy', 'PrincipalId', 'dummy')))]"
},
"resources": [
{ // Everything related to role assignments chokes completely, even if I unroll the loop
"dependsOn": [
"[variables('IdentityResourceId')]",
"storageaccountcopy"
],
"copy": {
"name": "RoleAssignmentsCopy",
"count": "[length(variables('ComputedRoleAssignments'))]"
}, // Conditionally deploy based on parameters, but use the value from computed variables
"condition": "[and(greater(length(parameters('RoleAssignments')), 0), parameters('Enabled'))]",
"type": "Microsoft.Storage/storageAccounts/providers/roleAssignments",
"apiVersion": "2020-04-01-preview",
"name": "[concat(variables('ComputedRoleAssignments')[copyIndex()].AccountName, '/Microsoft.Authorization/', guid(variables('ComputedRoleAssignments')[copyIndex()].Name))]",
"properties": {
"roleDefinitionId": "[variables('ComputedRoleAssignments')[copyIndex()].RoleDefinitionId]",
"principalId": "[if(and(empty(parameters('ComputedRoleAssignments')[copyIndex()].ResourceId), not(empty(parameters('ComputedRoleAssignments')[copyIndex()].PrincipalId))),
variables('ComputedRoleAssignments')[copyIndex()].PrincipalId,
reference(variables('ComputedRoleAssignments')[copyIndex()].ResourceId, '2018-11-30').PrincipalId)]",
"scope": "[variables('ComputedRoleAssignments')[copyIndex()].Scope]",
"principalType": "[variables('ComputedRoleAssignments')[copyIndex()].PrincipalType]"
}
},
{ // Everthing related to storage account provisioning works fine, this was existing code
"condition": "[and(greater(length(parameters('StorageAccounts')), 0), not(parameters('SkipStorageAccountProvisioning')), equals(parameters('StorageAccountProvisioningDefault'), 'true'))]",
"copy": {
"name": "storageaccountcopy",
"count": "[length(parameters('StorageAccounts'))]"
},
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[parameters('StorageAccounts')[copyIndex()].AccountName]",
"location": "[resourceGroup().location]",
"sku": {
"name": "[if(equals(parameters('StorageAccounts')[copyIndex()].AccountTypeOverride, parameters('StorageAccountTypeOverrideDefault')), parameters('StorageAccounts')[copyIndex()].AccountType, parameters('StorageAccounts')[copyIndex()].AccountTypeOverride)]",
"tier": "Standard"
},
"kind": "[parameters('StorageAccounts')[copyIndex()].AccountKind]",
"properties": {
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [],
"ipRules": [],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": true,
"encryption": {
"services": {
"file": {
"keyType": "Account",
"enabled": true
},
"blob": {
"keyType": "Account",
"enabled": true
}
},
"keySource": "Microsoft.Storage"
}
}
}
]
}