0

Azure function apps require access to a storage account so they may store host keys in a blob container.

The traditional way to provide this access is to include a configuration setting of AzureWebJobsStorage with a value of the connection string. However, in order to remove the need to manage the connection string (including the key), Microsoft introduced the ability to configure the function app to use its managed identity to access the storage account. This is described here

I have found that I can change an existing function app to use the managed identity approach. However, if I deploy a new app via bicep then the storage account container and host keys are not created. I've gone back to basics and used a simple sample bicep template from here

I updated the bicep so that a user managed identity is created. This is granted storage contributor and storage blob owner roles. The updated bicep is shown below:

@description('Specifies region of all resources.')
param location string = resourceGroup().location

@description('Suffix for function app, storage account, and key vault names.')
param appNameSuffix string = uniqueString(resourceGroup().id)

@description('Storage account SKU name.')
param storageSku string = 'Standard_LRS'

var functionAppName = 'fn-${appNameSuffix}'
var appServicePlanName = 'FunctionPlan'
var storageAccountName = 'fnstor${replace(appNameSuffix, '-', '')}'
var functionNameComputed = 'MyHttpTriggeredFunction'
var functionRuntime = 'dotnet'

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageSku
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    encryption: {
      services: {
        file: {
          keyType: 'Account'
          enabled: true
        }
        blob: {
          keyType: 'Account'
          enabled: true
        }
      }
      keySource: 'Microsoft.Storage'
    }
    accessTier: 'Hot'
  }
}


// Create Managed Identity for the funciton app
var managedIdName = 'id-${functionAppName}'
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdName
  location: location
}

var blobOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
resource roleDefinitionBlobContributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: resourceGroup()
  name: blobOwnerRoleId
}

// Role assignment
resource roleAssignmentBlob 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(functionAppName, blobOwnerRoleId)
  scope: storageAccount
  properties: {
    roleDefinitionId: roleDefinitionBlobContributor.id
    principalId: managedIdentity.properties.principalId
  }
}

var storageContributorRoleId = '17d1049b-9a84-46fb-8f53-869881c3d3ab'
resource roleDefinitionStorageContributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: resourceGroup()
  name: storageContributorRoleId
}

resource roleAssignmentContrib 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(functionAppName, storageContributorRoleId)
  scope: storageAccount
  properties: {
    roleDefinitionId: roleDefinitionStorageContributor.id
    principalId: managedIdentity.properties.principalId
  }
}

resource plan 'Microsoft.Web/serverfarms@2020-12-01' = {
  name: appServicePlanName
  location: location
  kind: 'functionapp'
  sku: {
    name: 'Y1'
  }
  properties: {}
}

resource functionApp 'Microsoft.Web/sites@2020-12-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentity.id}': {}
    }
  }
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage__accountname'
          value: storageAccountName
        }
        {
          name: 'AzureWebJobsStorage__blobServiceUri'
          value: 'https://${storageAccount.name}.blob.core.windows.net'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: functionRuntime
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
      ]
    }
    httpsOnly: true
  }
  dependsOn: [
    roleAssignmentBlob
  ]
}

Has anyone had any success in using bicep/arm to deploy a function app and storage account that uses managed identity?

Rob Bowman
  • 7,632
  • 22
  • 93
  • 200
  • 1
    I believe you would need to create a system assigned managed identity for the Function app and assign it appropriate RBAC roles on the storage account. – Gaurav Mantri Jul 07 '23 at 10:11
  • I tried system assigned before user assigned - same problem – Rob Bowman Jul 07 '23 at 11:36
  • That's weird. I have been able to do so successfully. I used a combination of ARM templates and PowerShell Cmdlets to do so. – Gaurav Mantri Jul 07 '23 at 11:48
  • Only different I see in my code and yours is that I am using `:` as the delimiter instead of `__`. So my setting is `AzureWebJobsStorage:blobServiceUri` instead of `AzureWebJobsStorage__blobServiceUri`. Can you please try with that? – Gaurav Mantri Jul 07 '23 at 11:53
  • 1
    Please note that using a User Assigned Identity [doesn't seem to be supported](https://github.com/MicrosoftDocs/azure-docs/issues/109682#event-9285376395). I see you have issues using a system assigned identity but please stick to a system assigned identity to solve any issues. – Peter Bons Jul 07 '23 at 12:12
  • @PeterBons with a system assigned managed id, it's created at the time the function app is created. I get the feeling that it's during the function app creation that the storage account container for the keys should be created. My thinking was this wouldn't be possible because there'd been no role assignment to the storage ac for the system managed id. I will go back and try again with system managed id now – Rob Bowman Jul 07 '23 at 14:32
  • @GauravMantri thanks for your suggestion. I tried both : and __ as the delimiter and that both work ok – Rob Bowman Jul 07 '23 at 14:56

1 Answers1

0

Thanks to @PeterBons - this is now working. I'm not sure why it didn't initially work when I used a system assigned managed identity. I have now deleted the resources, changed the bicep to use system managed id and re run. Please see corrected bicep below:

@description('Specifies region of all resources.')
param location string = resourceGroup().location
@description('Suffix for function app, storage account, and key vault names.')
param appNameSuffix string = uniqueString(resourceGroup().id)
@description('Storage account SKU name.')
param storageSku string = 'Standard_LRS'

var functionAppName = 'fn-${appNameSuffix}'
var appServicePlanName = 'FunctionPlan'
var storageAccountName = 'fnstor${replace(appNameSuffix, '-', '')}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageSku
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    encryption: {
      services: {
        file: {
          keyType: 'Account'
          enabled: true
        }
        blob: {
          keyType: 'Account'
          enabled: true
        }
      }
      keySource: 'Microsoft.Storage'
    }
    accessTier: 'Hot'
  }
}


// Role assignments
var blobOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
resource roleDefinitionBlobContributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: resourceGroup()
  name: blobOwnerRoleId
}

resource roleAssignmentBlob 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(functionAppName, blobOwnerRoleId)
  scope: storageAccount
  properties: {
    roleDefinitionId: roleDefinitionBlobContributor.id
    principalId: functionApp.identity.principalId
  }
}

var storageContributorRoleId = '17d1049b-9a84-46fb-8f53-869881c3d3ab'
resource roleDefinitionStorageContributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: resourceGroup()
  name: storageContributorRoleId
}

resource roleAssignmentContrib 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(functionAppName, storageContributorRoleId)
  scope: storageAccount
  properties: {
    roleDefinitionId: roleDefinitionStorageContributor.id
    principalId: functionApp.identity.principalId
  }
}

resource plan 'Microsoft.Web/serverfarms@2020-12-01' = {
  name: appServicePlanName
  location: location
  kind: 'functionapp'
  sku: {
    name: 'Y1'
  }
  properties: {}
}

resource functionApp 'Microsoft.Web/sites@2020-12-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage__accountname'
          value: storageAccountName
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'dotnet'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
      ]
    }
    httpsOnly: true
  }
}
Rob Bowman
  • 7,632
  • 22
  • 93
  • 200