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
.
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=