存储账户私有后Azure Function App无法访问问题排查
问题排查与修复:EP1计划Azure函数应用VNET集成+专用终结点无法访问存储账户
问题现象
创建基于EP1计划的Azure函数应用,启用VNET集成与专用终结点,关联的存储账户也启用专用终结点并禁用公网访问。部署后应用无法访问,运行时版本显示错误;将存储账户网络设为公开后,函数应用恢复正常。
核心问题分析
- 存储账户专用终结点不完整:函数应用依赖存储账户的多个服务(Blob、Queue、Table、File),当前仅为File服务创建了专用终结点,导致AzureWebJobsStorage无法正常访问其他存储服务。
- AzureWebJobsStorage配置错误:应用设置中
AzureWebJobsStorage被错误设置为存储账户的shared_access_key_enabled布尔值,实际需要存储账户的连接字符串。 - 存储账户网络规则未包含VNET集成子网:函数应用通过VNET集成子网(subnet_vnet_integration)发起请求,但该子网未被添加到存储账户的允许访问列表中。
- 缺少存储账户对应的私有DNS区域:未配置存储服务的私有DNS区域,导致函数应用无法解析存储账户的私有IP地址。
修正后的Terraform代码
resource "random_string" "suffix" { length = 6 upper = false special = false numeric = true } resource "azurerm_resource_group" "rg" { name = "rg-secure-funcapp" location = "East US" } resource "azurerm_virtual_network" "vnet" { name = "vnet-funcapp" address_space = ["10.0.0.0/16"] location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name } resource "azurerm_subnet" "subnet_pe" { name = "subnet-private-endpoints" resource_group_name = azurerm_resource_group.rg.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefixes = ["10.0.1.0/24"] private_endpoint_network_policies_enabled = false } resource "azurerm_subnet" "subnet_vnet_integration" { name = "subnet-vnet-integration" resource_group_name = azurerm_resource_group.rg.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefixes = ["10.0.3.0/24"] delegation { name = "delegation" service_delegation { name = "Microsoft.Web/serverFarms" actions = [ "Microsoft.Network/virtualNetworks/subnets/action", ] } } } resource "azurerm_storage_account" "storage" { name = "funcstorage${random_string.suffix.result}" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location account_tier = "Standard" account_replication_type = "LRS" allow_nested_items_to_be_public = false min_tls_version = "TLS1_2" network_rules { default_action = "Deny" virtual_network_subnet_ids = [azurerm_subnet.subnet_pe.id, azurerm_subnet.subnet_vnet_integration.id] bypass = ["AzureServices"] } } # 创建存储账户所需的私有DNS区域 resource "azurerm_private_dns_zone" "storage_blob" { name = "privatelink.blob.core.windows.net" resource_group_name = azurerm_resource_group.rg.name } resource "azurerm_private_dns_zone" "storage_queue" { name = "privatelink.queue.core.windows.net" resource_group_name = azurerm_resource_group.rg.name } resource "azurerm_private_dns_zone" "storage_table" { name = "privatelink.table.core.windows.net" resource_group_name = azurerm_resource_group.rg.name } resource "azurerm_private_dns_zone" "storage_file" { name = "privatelink.file.core.windows.net" resource_group_name = azurerm_resource_group.rg.name } # 将私有DNS区域关联到VNET resource "azurerm_private_dns_zone_virtual_network_link" "storage_blob_link" { name = "blob-dns-link" resource_group_name = azurerm_resource_group.rg.name private_dns_zone_name = azurerm_private_dns_zone.storage_blob.name virtual_network_id = azurerm_virtual_network.vnet.id } resource "azurerm_private_dns_zone_virtual_network_link" "storage_queue_link" { name = "queue-dns-link" resource_group_name = azurerm_resource_group.rg.name private_dns_zone_name = azurerm_private_dns_zone.storage_queue.name virtual_network_id = azurerm_virtual_network.vnet.id } resource "azurerm_private_dns_zone_virtual_network_link" "storage_table_link" { name = "table-dns-link" resource_group_name = azurerm_resource_group.rg.name private_dns_zone_name = azurerm_private_dns_zone.storage_table.name virtual_network_id = azurerm_virtual_network.vnet.id } resource "azurerm_private_dns_zone_virtual_network_link" "storage_file_link" { name = "file-dns-link" resource_group_name = azurerm_resource_group.rg.name private_dns_zone_name = azurerm_private_dns_zone.storage_file.name virtual_network_id = azurerm_virtual_network.vnet.id } # 为存储账户的所有必要服务创建专用终结点 resource "azurerm_private_endpoint" "storage_blob" { name = "pep-storage-blob" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name subnet_id = azurerm_subnet.subnet_pe.id private_service_connection { name = "psc-blob" private_connection_resource_id = azurerm_storage_account.storage.id subresource_names = ["blob"] is_manual_connection = false } private_dns_zone_group { name = "blob-dns-group" private_dns_zone_ids = [azurerm_private_dns_zone.storage_blob.id] } } resource "azurerm_private_endpoint" "storage_queue" { name = "pep-storage-queue" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name subnet_id = azurerm_subnet.subnet_pe.id private_service_connection { name = "psc-queue" private_connection_resource_id = azurerm_storage_account.storage.id subresource_names = ["queue"] is_manual_connection = false } private_dns_zone_group { name = "queue-dns-group" private_dns_zone_ids = [azurerm_private_dns_zone.storage_queue.id] } } resource "azurerm_private_endpoint" "storage_table" { name = "pep-storage-table" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name subnet_id = azurerm_subnet.subnet_pe.id private_service_connection { name = "psc-table" private_connection_resource_id = azurerm_storage_account.storage.id subresource_names = ["table"] is_manual_connection = false } private_dns_zone_group { name = "table-dns-group" private_dns_zone_ids = [azurerm_private_dns_zone.storage_table.id] } } resource "azurerm_private_endpoint" "storage_file" { name = "pep-storage-file" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name subnet_id = azurerm_subnet.subnet_pe.id private_service_connection { name = "psc-file" private_connection_resource_id = azurerm_storage_account.storage.id subresource_names = ["file"] is_manual_connection = false } private_dns_zone_group { name = "file-dns-group" private_dns_zone_ids = [azurerm_private_dns_zone.storage_file.id] } } resource "azurerm_storage_share" "share" { name = "fileshares" storage_account_id = azurerm_storage_account.storage.id quota = 5120 depends_on = [azurerm_private_endpoint.storage_file] } resource "azurerm_service_plan" "asp" { name = "asp-funcapp-ep1" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name os_type = "Windows" sku_name = "EP1" maximum_elastic_worker_count = 20 worker_count = 1 zone_balancing_enabled = false } resource "azurerm_windows_function_app" "func" { name = "funcapp-${random_string.suffix.result}" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name service_plan_id = azurerm_service_plan.asp.id storage_account_name = azurerm_storage_account.storage.name storage_account_access_key = azurerm_storage_account.storage.primary_access_key functions_extension_version = "~4" virtual_network_subnet_id = azurerm_subnet.subnet_vnet_integration.id site_config { always_on = true vnet_route_all_enabled = true scm_use_main_ip_restriction = true } app_settings = { AzureWebJobsStorage = azurerm_storage_account.storage.primary_connection_string WEBSITE_RUN_FROM_PACKAGE = "1" FUNCTIONS_WORKER_RUNTIME = "dotnet-isolated" WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = azurerm_storage_account.storage.primary_connection_string WEBSITE_CONTENTSHARE = azurerm_storage_share.share.name WEBSITE_VNET_ROUTE_ALL = "1" WEBSITE_DNS_SERVER = "168.63.129.16" WEBSITE_CONTENTOVERVNET = "1" } identity { type = "SystemAssigned" } } resource "azurerm_private_endpoint" "func_pe" { name = "pe-funcapp-1" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name subnet_id = azurerm_subnet.subnet_pe.id private_service_connection { name = "psc-funcapp-1" private_connection_resource_id = azurerm_windows_function_app.func.id subresource_names = ["sites"] is_manual_connection = false } } resource "azurerm_private_dns_zone" "privatedns" { name = "privatelink.azurewebsites.net" resource_group_name = azurerm_resource_group.rg.name } resource "azurerm_private_dns_zone_virtual_network_link" "dnslink" { name = "dns-link" resource_group_name = azurerm_resource_group.rg.name private_dns_zone_name = azurerm_private_dns_zone.privatedns.name virtual_network_id = azurerm_virtual_network.vnet.id } resource "azurerm_private_dns_a_record" "dnsrecord" { name = azurerm_windows_function_app.func.name zone_name = azurerm_private_dns_zone.privatedns.name resource_group_name = azurerm_resource_group.rg.name ttl = 300 records = [azurerm_private_endpoint.func_pe.private_service_connection[0].private_ip_address] }
关键修正说明
- 补全存储专用终结点:新增Blob、Queue、Table服务的专用终结点,并关联对应的私有DNS区域,确保函数应用能解析存储的私有IP。
- 修正AzureWebJobsStorage配置:将该值替换为存储账户的主连接字符串,解决配置类型错误问题。
- 更新存储网络规则:添加VNET集成子网到存储账户的允许访问列表,允许函数应用通过集成子网访问存储。
- 清理冗余配置:移除重复的
vnetrouteallenabled配置项,避免配置冲突。
内容的提问来源于stack exchange,提问作者Uday Kiran
相关产品推荐
相关产品推荐

