我正在使用 Bicep 和 Azure Pipelines 部署 Azure Function。基础设施部署和管道执行均成功完成。但是,当我导航到门户中的 Azure Function 时,我看到我的测试 API 按预期列出。但是当我尝试检索 API URL(带有函数代码的 URL)时,Azure 返回一个
500 Internal Server Error
。
此外,我注意到部署不一致:尽管该函数链接到存储帐户并使用 zip 部署进行部署,但存储帐户的容器不包含预期的元数据或工件。这与我对典型功能部署的期望不符。
以下是设置的相关文件:
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.bicep
@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'
我尝试过的:
测试存储帐户连接:我从 Kudu 控制台运行 nslookup 和curl,以检查 Azure Function 是否可以访问存储帐户。一切看起来都很好;存储帐户可以正常访问。
四重检查的应用程序设置:我多次检查了函数应用程序设置中的 AzureWebJobsStorage 连接字符串。它已正确设置,使用托管身份验证,如下所示:
DefaultEndpointsProtocol=https;AccountName=<obfuscated-name>;EndpointSuffix=core.windows.net;Authentication=Managed Identity
。我也尝试过不使用托管身份。
检查存储帐户的部署工件:部署后,我查看了存储帐户的 blob 容器,希望看到一些元数据或部署文件,但它完全是空的。就像部署没有在那里注册任何东西一样。
尝试直接部署:为了排除管道的任何问题,我直接使用 Azure CLI 部署了该函数。相同的结果 — 尝试获取函数的 URL 时出现 500 错误,并且存储帐户仍然没有任何部署数据。
审查基础设施配置:我检查了用于部署基础设施的所有 Bicep 文件,以确保存储帐户、功能应用程序或权限的设置方式没有任何问题。我没有发现任何明显错误。
检查托管身份设置:我确认 Function App 的托管身份已将正确的存储 Blob 数据贡献者角色分配给存储帐户。权限看起来不错。
根据提供的信息,我可以识别可能导致 500 错误和丢失存储工件的几个潜在问题。让我们系统地解决它们:
publicNetworkAccess: 'Disabled'
并使用专用终结点,但可能存在一些网络配置问题:// Add this to your function app configuration
resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
// ... existing configuration ...
properties: {
siteConfig: {
// Add these settings
privateEndpointsEnabled: true
ipSecurityRestrictions: [
{
vnetSubnetResourceId: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName, functionSubnetName)
action: 'Allow'
priority: 100
name: 'Allow VNet'
}
]
}
}
}
resource functionStorageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(storageAccount.id, functionApp.id, 'Storage Blob Data Owner')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')
principalId: functionApp.identity.principalId
principalType: 'ServicePrincipal'
}
scope: storageAccount
}
resource functionQueueRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(storageAccount.id, functionApp.id, 'Storage Queue Data Contributor')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
principalId: functionApp.identity.principalId
principalType: 'ServicePrincipal'
}
scope: storageAccount
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};Authentication=Managed Identity'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: toLower(functionAppName)
}
{
name: 'WEBSITE_CONTENTOVERVNET'
value: '1'
}
deployment-template.yml
中更新您的部署任务:- 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'
deployToSlotOrASE: false
enableCustomDeployment: true
additionalArguments: '-UseMangedIdentity true'
resource privateDnsZoneSites 'Microsoft.Network/privateDnsZones@2018-09-01' = {
name: 'privatelink.azurewebsites.net'
location: 'global'
}
resource dnsZoneVnetLinkSites 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2018-09-01' = {
name: '${privateDnsZoneSites.name}/${vnetName}-link'
location: 'global'
properties: {
virtualNetwork: {
id: resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks', vnetName)
}
registrationEnabled: false
}
}
故障排除步骤 实施这些更改后:
检查函数应用的诊断日志:
az functionapp logs tail --name <function-app-name> --resource-group <resource-group>
# From Kudu console (https://<function-app-name>.scm.azurewebsites.net/DebugConsole)
curl -v https://<storage-account-name>.blob.core.windows.net
az functionapp identity show --name <function-app-name> --resource-group <resource-group>
这些更改应该可以解决 500 错误并确保正确的存储帐户集成。解决的要点是:
如果您需要任何说明或在实施这些更改后遇到其他问题,请告诉我。