You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

存储账户私有后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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 07:07:33