带有 Bicep 的 Azure 函数:部署成功完成,但函数 URL 返回 500 错误

问题描述 投票:0回答:1

我正在使用 Bicep 和 Azure Pipelines 部署 Azure Function。基础设施部署和管道执行均成功完成。但是,当我导航到门户中的 Azure Function 时,我看到我的测试 API 按预期列出。但是当我尝试检索 API URL(带有函数代码的 URL)时,Azure 返回一个

500 Internal Server Error

尝试获取 url 时,azure 函数出错

此外,我注意到部署不一致:尽管该函数链接到存储帐户并使用 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 数据贡献者角色分配给存储帐户。权限看起来不错。

azure azure-devops azure-functions azure-pipelines azure-bicep
1个回答
0
投票

根据提供的信息,我可以识别可能导致 500 错误和丢失存储工件的几个潜在问题。让我们系统地解决它们:

  1. 专用网络配置 您的存储帐户配置为
    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'
        }
      ]
    }
  }
}
  1. 存储帐户角色分配 您需要确保函数应用的托管标识具有正确的角色。添加这些角色分配:
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
}
  1. 功能应用程序配置 将这些附加应用设置添加到您的函数应用中:
{
  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'
}
  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'
  1. DNS 区域配置 为函数应用添加私有 DNS 区域:
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
  }
}
  1. 故障排除步骤 实施这些更改后:

  2. 检查函数应用的诊断日志:

az functionapp logs tail --name <function-app-name> --resource-group <resource-group>
  1. 验证网络连接:
# From Kudu console (https://<function-app-name>.scm.azurewebsites.net/DebugConsole)
curl -v https://<storage-account-name>.blob.core.windows.net
  1. 检查托管身份分配:
az functionapp identity show --name <function-app-name> --resource-group <resource-group>

这些更改应该可以解决 500 错误并确保正确的存储帐户集成。解决的要点是:

  • 正确的专用网络配置
  • 函数应用的托管标识所需的角色分配
  • 其他所需的应用程序设置
  • 专用端点的 DNS 配置
  • 管道部署配置

如果您需要任何说明或在实施这些更改后遇到其他问题,请告诉我。

© www.soinside.com 2019 - 2024. All rights reserved.