Azure Function with Bicep: Deploy Completes Successfully, But Function URL Returns 500 Error

Andrea Rubino 0 Reputation points
2024-11-29T14:10:22.39+00:00

I'm deploying an Azure Function using Bicep and Azure Pipelines. Both the infrastructure deployment and the pipeline execution complete successfully. However, when I navigate to the Azure Function in the portal, I see my test API listed as expected. But when I try to retrieve the API URL (the one with the function code), Azure returns a 500 Internal Server Error.

mL5kRvYD

Additionally, I noticed an inconsistency with the deployment: although the function is linked to a storage account and deployed using zip deployment, the storage account's container doesn't contain the expected metadata or artifacts. This doesn't align with what I would expect from a typical function deployment.

Below are the relevant files for the setup:

main.bicep


@description('The environment for the function to be deployed in')
@allowed([
  'dev'
  'stg'
  'prd'
])
param env string = 'dev'

@description('Location for all resources unless otherwise specified.')
param location string = resourceGroup().location

@description('The instance number for the current function to be deployed')
@minLength(2)
@maxLength(2)
param instance string = '01'

@description('VPN IP address for firewall rule')
param vpnIpAddress string

@description('Location for Log Analytics Workspace')
param logAnalyticsLocation string = 'westeurope'

@description('Group ID for developers to assign roles')
param devGroupId string

@description('The project name')
@minLength(4)
param projectName string

@description('The name of the Virtual Network')
param vnetName string

@description('The name of the subnet for the storage account')
param storageSubnetName string = 'storage-subnet'

@description('The name of the subnet for the function app')
param functionSubnetName string

@description('The resource group of the VNet')
param vnetResourceGroup string

@description('The App Registration (Service Principal) ID for the pipeline service connection')
param serviceConnectionObjectId string

var resourceGroupTags = resourceGroup().tags

module logAnalyticsWorkspace './modules/log_analytics_workspace.bicep' = {
  name: 'logAnalyticsDeployment'
  params: {
    projectName: projectName
    location: logAnalyticsLocation
    env: env
    devGroupId: devGroupId
    tags: resourceGroupTags
  }
}

module function './modules/function.bicep' = {
  name: 'functionDeployment'
  params: {
    projectName: projectName
    env: env
    instance: instance
    location: location
    vpnIpAddress: vpnIpAddress
    logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.logAnalyticsId
    devGroupId: devGroupId
    tags: resourceGroupTags
    vnetName: vnetName
    storageSubnetName: storageSubnetName
    vnetResourceGroup: vnetResourceGroup
    functionSubnetName: functionSubnetName
    serviceConnectionObjectId: serviceConnectionObjectId
  }
}

function.bice

@description('The name of the project')
@minLength(4)
param projectName string

@description('The environment for the function to be deployed in')
param env string

@description('The instance number for the function')
@minLength(2)
@maxLength(2)
param instance string

@description('Location for the resources')
param location string

@description('VPN IP address for firewall rule')
param vpnIpAddress string

@description('Log Analytics Workspace ID for monitoring')
param logAnalyticsWorkspaceId string

@description('Group ID for developers')
param devGroupId string

@description('Tags to associate with resources')
param tags object

@description('The Virtual Network name')
param vnetName string

@description('The subnet for the storage account')
param storageSubnetName string

@description('The resource group of the VNet')
param vnetResourceGroup string

@description('The name of the subnet for the function app')
param functionSubnetName string = 'storage-subnet'

@description('The App Registration (Service Principal) ID for the pipeline service connection')
param serviceConnectionObjectId string

var functionAppName = '${projectName}-${env}-${instance}-func'
var hostingPlanName = 'ASP-${projectName}-${env}-${instance}'
var storageAccountName = toLower('${projectName}${env}${instance}funcst')
var applicationInsightsName = '${projectName}-${env}-${instance}-insights'

@description('Variables that change based on the environment')
var envSpecifics = {
  dev: {
    storageAccountType: 'Standard_LRS'
    hostingPlanSkuSpecs: {
      name: 'Y1'
      tier: 'Dynamic'
    }
  }
  stg: {
    storageAccountType: 'Standard_GRS'
    hostingPlanSkuSpecs: {
      name: 'P2V2'
      tier: 'PremiumV2'
    }
  }
  prd: {
    storageAccountType: 'Standard_RAGRS'
    hostingPlanSkuSpecs: {
      name: 'P2V2'
      tier: 'PremiumV2'
    }
  }
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspaceId
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: envSpecifics[env].storageAccountType
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    allowSharedKeyAccess: false
    publicNetworkAccess: 'Disabled'
    networkAcls: {
      defaultAction: 'Deny'
      virtualNetworkRules: [
        {
          id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName, functionSubnetName)
        }
        {
          id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName, storageSubnetName)
        }
      ]
      bypass: 'AzureServices'
    }
  }
  tags: tags
}

resource storageAccountRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storageAccount.id, devGroupId, 'Storage Blob Data Contributor')
  properties: {
    roleDefinitionId: subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
    )
    principalId: devGroupId
    principalType: 'Group'
  }
  scope: storageAccount
}

resource serviceConnectionRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storageAccount.id, serviceConnectionObjectId, 'Contributor')
  properties: {
    roleDefinitionId: subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      'b24988ac-6180-42a0-ab88-20f7382dd24c'
    )
    principalId: serviceConnectionObjectId
    principalType: 'ServicePrincipal'
  }
  scope: storageAccount
}

resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = {
  name: '${storageAccountName}-private-endpoint'
  location: location
  properties: {
    subnet: {
      id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName, storageSubnetName)
    }
    privateLinkServiceConnections: [
      {
        name: '${storageAccountName}-connection'
        properties: {
          privateLinkServiceId: storageAccount.id
          groupIds: [
            'blob'
          ]
        }
      }
    ]
  }
}

resource hostingPlan 'Microsoft.Web/serverfarms@2022-09-01' = {
  name: hostingPlanName
  location: location
  sku: {
    name: envSpecifics[env].hostingPlanSkuSpecs.name
    tier: envSpecifics[env].hostingPlanSkuSpecs.tier
  }
  properties: {
    reserved: true
  }
  tags: tags
}

resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp,linux'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    reserved: true
    serverFarmId: hostingPlan.id
    storageAccountRequired: true
    siteConfig: {
      linuxFxVersion: 'python|3.10'
      appSettings: [
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'python'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'WEBSITE_ALWAYS_ON'
          value: 'true'
        }
        {
          name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
          value: 'true'
        }
        {
          name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
          value: 'true'
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: appInsights.properties.InstrumentationKey
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: appInsights.properties.ConnectionString
        }
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};Authentication=Managed Identity'
        }
      ]
      vnetRouteAllEnabled: true
    }
    virtualNetworkSubnetId: resourceId(
      vnetResourceGroup,
      'Microsoft.Network/virtualNetworks/subnets',
      vnetName,
      functionSubnetName
    )
    httpsOnly: true
  }
  tags: tags
}

resource privateDnsZoneBlob 'Microsoft.Network/privateDnsZones@2018-09-01' = {
  name: 'privatelink.blob.core.windows.net'
  location: 'global'
  properties: {}
}

resource privateDnsZoneFile 'Microsoft.Network/privateDnsZones@2018-09-01' = {
  name: 'privatelink.file.core.windows.net'
  location: 'global'
  properties: {}
}

resource dnsZoneVnetLinkBlob 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2018-09-01' = {
  name: '${privateDnsZoneBlob.name}/${vnetName}-link'
  location: 'global'
  properties: {
    virtualNetwork: {
      id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks', vnetName)
    }
    registrationEnabled: false
  }
}

resource dnsZoneVnetLinkFile 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2018-09-01' = {
  name: '${privateDnsZoneFile.name}/${vnetName}-link'
  location: 'global'
  properties: {
    virtualNetwork: {
      id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks', vnetName)
    }
    registrationEnabled: false
  }
}

resource storagePrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-03-01' = {
  name: '${storagePrivateEndpoint.name}/default'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'blob-dns-zone-config'
        properties: {
          privateDnsZoneId: privateDnsZoneBlob

azure-pipelines.yml

trigger:
  branches:
    include:
    - '*'

variables:
  vmImageName: 'ubuntu-22.04'
  pythonVersion: '3.10'
  appName: 'my-app'
  instance: '01'

pool:
  vmImage: $(vmImageName)

stages:
- stage: Validate
  jobs:
  - job: RunValidation
    steps:
    - task: UsePythonVersion@0
      inputs:
        versionSpec: $(pythonVersion)
    - script: |
        pip install -r __app__/requirements.txt
        python -m compileall -f __app__
        python -m compileall -f tests
      displayName: 'Validate and Compile'

- stage: Build
  jobs:
  - job: BuildApp
    steps:
    - task: UsePythonVersion@0
      inputs:
        versionSpec: $(pythonVersion)
    - script: pip install --target="__app__/.python_packages/lib/site-packages" -r __app__/requirements.txt
      displayName: 'Install dependencies'
    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: $(Build.Repository.LocalPath)/__app__
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(appName)-api-$(Build.BuildId).zip
    - task: PublishBuildArtifacts@1
      inputs:
        pathtoPublish: '$(Build.ArtifactStagingDirectory)' 
        artifactName: '$(appName)-api'

- template: deployment-template.yml
  parameters:
    environment: dev
    branch: develop
    resourceGroup: my-rg-dev
    serviceConnection: my-azure-connection

deployment-template.yml

parameters:
- name: environment
  type: string
- name: branch
  type: string
- name: resourceGroup
  type: string
- name: serviceConnection
  type: string

stages:
- stage: Deploy_${{ parameters.environment }}
  condition: and(succeeded('Build'), eq(variables['build.sourceBranch'], format('refs/heads/{0}', '${{ parameters.branch }}')))
  jobs:
    - deployment: Deploy_${{ parameters.environment }}
      environment: ${{ parameters.environment }}
      strategy:
        runOnce:
          deploy:
            steps:
              - checkout: self
              - task: DownloadBuildArtifacts@0
                inputs:
                  buildType: 'current'
                  artifactName: '$(appName)-api'
              - task: AzureFunctionApp@1
                inputs:
                  azureSubscription: ${{ parameters.serviceConnection }}
                  appType: 'functionAppLinux'
                  appName: '$(appName)-${{ parameters.environment }}-$(instance)-func'
                  package: '$(System.ArtifactsDirectory)/$(appName)-api/$(appName)-api-$(Build.BuildId).zip'
                  deploymentMethod: 'zipDeploy'

What I've tried:

Tested Storage Account Connectivity: I ran nslookup and curl from the Kudu console to check if the Azure Function could reach the storage account. Everything seems fine; the storage account is reachable without any issues.

Quadruple-Checked App Settings: I went through the AzureWebJobsStorage connection string in the function's app settings multiple times. It’s set up correctly, using managed identity authentication like this: `DefaultEndpointsProtocol=https;AccountName=

Azure Functions
Azure Functions
An Azure service that provides an event-driven serverless compute platform.
5,189 questions
Azure Storage Accounts
Azure Storage Accounts
Globally unique resources that provide access to data management services and serve as the parent namespace for the services.
3,267 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Pinaki Ghatak 5,310 Reputation points Microsoft Employee
    2024-12-04T10:53:09.55+00:00

    Hello @Andrea Rubino

    Here are some additional steps you can take to troubleshoot the issue:

    1. Check the logs: You can check the logs for the Azure Function to see if there are any errors or exceptions being thrown. You can access the logs from the Azure portal by going to your function app, selecting "Functions" from the left-hand menu, selecting your function, and then selecting "Logs" from the top menu.
    2. Check the storage account firewall settings: Make sure that the storage account firewall settings are configured correctly to allow traffic from the Azure Function. You can check this by going to the storage account in the Azure portal, selecting "Firewalls and virtual networks" from the left-hand menu, and then making sure that the "Allow access from" setting is set to "Selected networks" and that the IP address of the Azure Function is included in the list of allowed IP addresses.
    3. Check the storage account access keys: Make sure that the storage account access keys are configured correctly in the Azure Function app settings. You can check this by going to the Azure Function in the Azure portal, selecting "Configuration" from the left-hand menu, and then making sure that the "AzureWebJobsStorage" setting is set to the correct connection string for the storage account.
    4. Check the storage account container permissions: Make sure that the storage account container permissions are set correctly to allow the Azure Function to access the container. You can check this by going to the storage account in the Azure portal, selecting "Containers" from the left-hand menu, selecting the container that should contain the deployment artifacts, and then selecting "Access policy" from the top menu.
    5. Make sure that the "Public access level" is set to "Private (no anonymous access)" and that the "Allowed access level" is set to "Blob (anonymous read access for blobs only)".

    I hope that this response has addressed your query and helped you overcome your challenges. If so, please mark this response as Answered. This will not only acknowledge our efforts, but also assist other community members who may be looking for similar solutions.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.