1

Trying to get azurerm_linux_function_app to work with azurerm_storage_account_network_rules and azurerm_private_endpoint.

Terraform v1.3.7
on darwin_arm64
+ provider registry.terraform.io/hashicorp/azurerm v3.38.0

This is what I'm trying.

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "func_rg" {
  name     = "func"
  location = "eastus"
}

# Create the network VNET
resource "azurerm_virtual_network" "func" {
  name                = "func_vnet"
  address_space       = ["10.0.0.0/16"]
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
}

# Create a subnet for endpoint
resource "azurerm_subnet" "endpoint_subnet" {
  name                                      = "endpoint_subnet"
  address_prefixes                          = ["10.0.0.0/24"]
  virtual_network_name                      = azurerm_virtual_network.func.name
  resource_group_name                       = azurerm_resource_group.func_rg.name
  private_endpoint_network_policies_enabled = true
}

# Create subnet for functions
resource "azurerm_subnet" "func_subnet" {
  name                 = "func_subnet"
  resource_group_name  = azurerm_resource_group.func_rg.name
  virtual_network_name = azurerm_virtual_network.func.name
  address_prefixes     = ["10.0.1.0/24"]
  service_endpoints    = ["Microsoft.Storage"]

  delegation {
    name = "delegation"

    service_delegation {
      name = "Microsoft.Web/serverFarms"
    }
  }
}

# Create nsg for subnets
resource "azurerm_network_security_group" "func" {
  name                = "func_nsg"
  location            = azurerm_resource_group.func_rg.location
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Associate nsg with func subnet
resource "azurerm_subnet_network_security_group_association" "func" {
  subnet_id                 = azurerm_subnet.func_subnet.id
  network_security_group_id = azurerm_network_security_group.func.id
}

# Associate nsg with endpoint subnet
resource "azurerm_subnet_network_security_group_association" "endpoint" {
  subnet_id                 = azurerm_subnet.endpoint_subnet.id
  network_security_group_id = azurerm_network_security_group.func.id
}

# Create blob Private DNS Zone
resource "azurerm_private_dns_zone" "blob_dns_zone" {
  name                = "privatelink.blob.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create file Private DNS Zone
resource "azurerm_private_dns_zone" "file_dns_zone" {
  name                = "privatelink.file.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create table Private DNS Zone
resource "azurerm_private_dns_zone" "table_dns_zone" {
  name                = "privatelink.table.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create queue Private DNS Zone
resource "azurerm_private_dns_zone" "queue_dns_zone" {
  name                = "privatelink.queue.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create blob Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "blob_vnl" {
  name                  = "blob_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.blob_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create file Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "file_vnl" {
  name                  = "file_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.file_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create table Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "table_vnl" {
  name                  = "table_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.table_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create queue Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "queue_vnl" {
  name                  = "queue_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.queue_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

resource "azurerm_storage_account" "func" {
  name                     = "rdtestfuncsa"
  resource_group_name      = azurerm_resource_group.func_rg.name
  location                 = azurerm_resource_group.func_rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_account_network_rules" "func" {
  storage_account_id = azurerm_storage_account.func.id

  default_action             = "Deny"
  ip_rules                   = ["***.***.***.***"]
  virtual_network_subnet_ids = [azurerm_subnet.func_subnet.id]
  bypass                     = ["Metrics", "Logging", "AzureServices"]
}

resource "azurerm_storage_container" "func" {
  name                 = "func-sc"
  storage_account_name = azurerm_storage_account.func.name
  container_access_type = "private"
}

# Create Private Endpints
resource "azurerm_private_endpoint" "file_endpoint" {
  name                = "rdtest_file_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_file_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["file"]
  }
}

resource "azurerm_private_endpoint" "blob_endpoint" {
  name                = "rdtest_blob_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_blob_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["blob"]
  }
}

resource "azurerm_private_endpoint" "table_endpoint" {
  name                = "rdtest_table_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_table_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["table"]
  }
}

resource "azurerm_private_endpoint" "queue_endpoint" {
  name                = "rdtest_queue_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_queue_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["queue"]
  }
}

# Create DNS A Records
resource "azurerm_private_dns_a_record" "blob_dns_a" {
  name                = "rdfunctestblobdns"
  zone_name           = azurerm_private_dns_zone.blob_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.blob_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "file_dns_a" {
  name                = "filedns"
  zone_name           = azurerm_private_dns_zone.file_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.file_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "table_dns_a" {
  name                = "tabledns"
  zone_name           = azurerm_private_dns_zone.table_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.table_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "queue_dns_a" {
  name                = "queuedns"
  zone_name           = azurerm_private_dns_zone.queue_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.queue_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_service_plan" "func" {
  name                = "func_sp"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  os_type             = "Linux"
  sku_name            = "EP1"
}

resource "azurerm_linux_function_app" "func" {
  name                       = "rd-test-func"
  resource_group_name        = azurerm_resource_group.func_rg.name
  location                   = azurerm_resource_group.func_rg.location
  virtual_network_subnet_id  = azurerm_subnet.func_subnet.id
  storage_account_name       = azurerm_storage_account.func.name
  storage_account_access_key = azurerm_storage_account.func.primary_access_key
  service_plan_id            = azurerm_service_plan.func.id
  site_config {
    application_stack {
      node_version = 16
    }
  }
}

I get this error from that config

│ Error: creating Linux Function App: (Site Name "rd-test-func" / Resource Group "func"): web.AppsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: Code="BadRequest" Message="Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible." Details=[{"Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible."},{"Code":"BadRequest"},{"ErrorEntity":{"Code":"BadRequest","ExtendedCode":"99022","Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.","MessageTemplate":"Creation of storage file share failed with: '{0}'. Please check if the storage account is accessible.","Parameters":["The remote server returned an error: (403) Forbidden."]}}]
│ 
│   with azurerm_linux_function_app.func,
│   on main.tf line 236, in resource "azurerm_linux_function_app" "func":
│  236: resource "azurerm_linux_function_app" "func" {
│ 
│ creating Linux Function App: (Site Name "rd-test-func" / Resource Group "func"): web.AppsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: Code="BadRequest" Message="Creation of storage
│ file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible." Details=[{"Message":"Creation of storage file share failed with: 'The remote server
│ returned an error: (403) Forbidden.'. Please check if the storage account is accessible."},{"Code":"BadRequest"},{"ErrorEntity":{"Code":"BadRequest","ExtendedCode":"99022","Message":"Creation of storage file share
│ failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.","MessageTemplate":"Creation of storage file share failed with: '{0}'. Please check if the
│ storage account is accessible.","Parameters":["The remote server returned an error: (403) Forbidden."]}}]

If I take out the azurerm_storage_account_network_rules, which I need for compliance, the config will deploy, but publishing the function fails on syncing triggers.

func azure functionapp publish rd-test-func
Getting site publishing info...
Uploading package...
Uploading 1.35 KB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Error calling sync triggers (BadRequest).

If I remove virtual_network_subnet_id = azurerm_subnet.func_subnet.id from azurerm_linux_function_app as well the function will publish, but I need the function app connecting over the private endpoint or with network rules.

Anyone know how to get this to work?

2 Answers2

1

Turns out azurerm_service_plan needs sku_name = "B1" or better for the vnet integration with azurerm_storage_account_network_rules to work. The following worked with B1, S1 and P1v2.

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "func_rg" {
  name     = "func"
  location = "eastus"
}

resource "azurerm_virtual_network" "func" {
  name                = "func_vnet"
  address_space       = ["10.0.0.0/16"]
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
}

resource "azurerm_subnet" "func_subnet" {
  name                 = "func_subnet"
  resource_group_name  = azurerm_resource_group.func_rg.name
  virtual_network_name = azurerm_virtual_network.func.name
  address_prefixes     = ["10.0.1.0/24"]
  service_endpoints    = ["Microsoft.Storage"]

  delegation {
    name = "delegation"

    service_delegation {
      name = "Microsoft.Web/serverFarms"
    }
  }
}

resource "azurerm_storage_account" "func" {
  name                     = "rdtestfuncsa"
  resource_group_name      = azurerm_resource_group.func_rg.name
  location                 = azurerm_resource_group.func_rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_account_network_rules" "func" {
  storage_account_id = azurerm_storage_account.func.id

  default_action             = "Deny"
  ip_rules                   = ["***.***.***.***"]
  virtual_network_subnet_ids = [azurerm_subnet.func_subnet.id]
  bypass                     = ["Metrics", "Logging", "AzureServices"]
}

resource "azurerm_service_plan" "func" {
  name                = "func_sp"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  os_type             = "Linux"
  sku_name            = "B1"
}

resource "azurerm_linux_function_app" "func" {
  name                       = "rd-test-func"
  resource_group_name        = azurerm_resource_group.func_rg.name
  location                   = azurerm_resource_group.func_rg.location
  virtual_network_subnet_id  = azurerm_subnet.func_subnet.id
  storage_account_name       = azurerm_storage_account.func.name
  storage_account_access_key = azurerm_storage_account.func.primary_access_key
  service_plan_id            = azurerm_service_plan.func.id

  site_config {
    application_stack {
      node_version = 16
    }
  }
}
0

the following should work.

You'll need to pre-create a share for the functions content you can name this whatever you want. Then add the following as app setting, to the functions configuration.

More info in these settings and why you'll need them https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings#website_contentshare

app_settings

WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = azurerm_storage_account.func.primary_connection_string    
AzureWebJobsStorage                      = azurerm_storage_account.func.primary_connection_string
WEBSITE_CONTENTSHARE                     = {name of pre-created share}
Tim Tharratt
  • 1,251
  • 1
  • 10
  • 18
  • Still getting the `Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.` error after adding share and adding this to the func app. ``` app_settings = { WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = azurerm_storage_account.func.primary_connection_string AzureWebJobsStorage = azurerm_storage_account.func.primary_connection_string WEBSITE_CONTENTSHARE = azurerm_storage_share.func.name } ``` – Accidental Admin Jan 09 '23 at 16:44