azureazure-functionsazure-blob-storageterraform-provider-azureprivate-endpoint

Azure Function App is not accessible when Storage Account not public


I am trying to create a function app - EP1 plan with the VNET integration and private endpoint enabled and also I have enabled the private endpoint enabled for the Storage account that used for function app creation ( both the public access disabled ) however, after deployment I see the app is not accessible and it shows error in the runtime version.

enter image description here

enter image description here

Also I the function app is working fine when I enable the storage account networking to public.

I have also gonna through the similar articles , but none of them helped.https://github.com/Azure/Azure-Functions/issues/1361#issuecomment-574686347

Here is the terraform code.

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"]
  service_endpoints    = ["Microsoft.Storage"]
}

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]
    bypass                     = ["AzureServices"]
  }

}

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_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_storage_share" "share" {
  name               = "fileshares"
  storage_account_id = azurerm_storage_account.storage.id
  quota              = 5120
  depends_on         = [azurerm_private_endpoint.storage_file]
}
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
  }
}

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.shared_access_key_enabled
      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"
      vnetrouteallenabled                      = true
    }
  
  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]
}

Solution

  • It seems to be that you have a problem with your DNS resolution. The "problem" behind private Endpoints ist that you need to configure the private DNS zones in the formats Microsoft needs the private DNS zone to get the handling be done in the background.

    What Microsoft does is the following:

    If you are not using the private DNS Zone names of Microsoft you have to do all this things by your own. My recommendation for you: Use the DNS zone names from Microsoft and let the private endpoints auto register themselves.

    In your case I would suggest the following endpoints with the corresponding private DNS Zones:

    Private Endpoint Private DNS Zone Name Sub Resource
    Function App privatelink.azurewebsites.net sites
    Storage Account - Blob privatelink.blob.core.windows.net blob
    Storage Account - File Share privatelink.file.core.windows.net file

    The official documentation for the private DNS Zones can be found at Azure Private Endpoint private DNS zone values

    Private Endpoint Definition in Terraform inclusive private DNS:

    resource "azurerm_private_dns_zone" "private_dns_zone_func" {
      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.private_dns_zone_func.name
      virtual_network_id    = azurerm_virtual_network.vnet.id
    }
    
    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
      }
    
      private_dns_zone_group {
        name                 = "dnszg-funcapp-1"
        private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_func.id]
      }
    }
    

    This configuration needs to be done for all the types documented in the table above. But be aware that you need to be connected to an Azure network to access your function because the public access should be disabled if you configure a private endpoint. Otherwise you can get rid of the private endpoint of the function an access the function over the public internet.

    Your function should look like this with private endpoint in use:

    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
      public_network_access_enabled = false
    
      site_config {
        always_on                   = true
      }
    
      app_settings = {
          AzureWebJobsStorage   = azurerm_storage_account.storage.shared_access_key_enabled
          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
          vnetrouteallenabled                      = true
        }
      
      identity {
        type = "SystemAssigned"
      }
    
    }
    

    Youe Storage account should look like this for use with private endpoint:

    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"
      public_network_access_enabled = false
    }
    

    And one last thing I would recommend is to use IAM (RBAC) permissions to access the Storage Account. You already enabled a System assigned Identity for your function. The only thing you need to include is a role assignment at the storage account to work with files and blobs.